diff --git a/CMakeLists.txt b/CMakeLists.txt index dfcfe46..a0397b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,8 +22,8 @@ if(ENABLE_FRONTEND_API) endif() if(ENABLE_QT) - find_qt(COMPONENTS Widgets Core Concurrent) - target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE Qt::Core Qt::Widgets Qt::Concurrent) + find_qt(COMPONENTS Widgets Core Concurrent Svg) + target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE Qt::Core Qt::Widgets Qt::Concurrent Qt::Svg) target_compile_options( ${CMAKE_PROJECT_NAME} PRIVATE $<$:-Wno-quoted-include-in-framework-header -Wno-comma>) @@ -65,6 +65,8 @@ target_sources( src/scene-bundle.hpp src/setup-wizard.cpp src/setup-wizard.hpp + src/scene-collection-info.cpp + src/scene-collection-info.hpp src/export-wizard.cpp src/export-wizard.hpp src/elgato-product.cpp diff --git a/data/images/IconFile.svg b/data/images/IconFile.svg new file mode 100644 index 0000000..69e8623 --- /dev/null +++ b/data/images/IconFile.svg @@ -0,0 +1,3 @@ + + + diff --git a/data/images/IconImage.svg b/data/images/IconImage.svg new file mode 100644 index 0000000..c3b239c --- /dev/null +++ b/data/images/IconImage.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/images/IconLogoCss.svg b/data/images/IconLogoCss.svg new file mode 100644 index 0000000..79615b9 --- /dev/null +++ b/data/images/IconLogoCss.svg @@ -0,0 +1,3 @@ + + + diff --git a/data/images/IconLogoHtml.svg b/data/images/IconLogoHtml.svg new file mode 100644 index 0000000..25e3555 --- /dev/null +++ b/data/images/IconLogoHtml.svg @@ -0,0 +1,3 @@ + + + diff --git a/data/images/IconLogoJs.svg b/data/images/IconLogoJs.svg new file mode 100644 index 0000000..8847a2f --- /dev/null +++ b/data/images/IconLogoJs.svg @@ -0,0 +1,3 @@ + + + diff --git a/data/images/IconLogoLua.svg b/data/images/IconLogoLua.svg new file mode 100644 index 0000000..f60e09d --- /dev/null +++ b/data/images/IconLogoLua.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/images/IconVideo.svg b/data/images/IconVideo.svg new file mode 100644 index 0000000..47084d6 --- /dev/null +++ b/data/images/IconVideo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/images/IconVolume.svg b/data/images/IconVolume.svg new file mode 100644 index 0000000..102e274 --- /dev/null +++ b/data/images/IconVolume.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/data/images/button-download.svg b/data/images/button-download.svg new file mode 100644 index 0000000..1440593 --- /dev/null +++ b/data/images/button-download.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/images/button-marketplace-icon.svg b/data/images/button-marketplace-icon.svg new file mode 100644 index 0000000..eeba9f3 --- /dev/null +++ b/data/images/button-marketplace-icon.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/data/images/button-settings-icon-disabled.svg b/data/images/button-settings-icon-disabled.svg new file mode 100644 index 0000000..1682723 --- /dev/null +++ b/data/images/button-settings-icon-disabled.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/images/button-settings-icon.svg b/data/images/button-settings-icon.svg new file mode 100644 index 0000000..10e94e0 --- /dev/null +++ b/data/images/button-settings-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/images/button-stop-download.svg b/data/images/button-stop-download.svg new file mode 100644 index 0000000..a4d5cd2 --- /dev/null +++ b/data/images/button-stop-download.svg @@ -0,0 +1,3 @@ + + + diff --git a/data/images/camera-icon-sm.png b/data/images/camera-icon-sm.png new file mode 100644 index 0000000..b55b5a2 --- /dev/null +++ b/data/images/camera-icon-sm.png @@ -0,0 +1,4 @@ + + + + diff --git a/data/images/camera-placeholder-icon.svg b/data/images/camera-placeholder-icon.svg new file mode 100644 index 0000000..1440d25 --- /dev/null +++ b/data/images/camera-placeholder-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/data/images/checkbox_checked.png b/data/images/checkbox_checked.png index 089d1e0..67cb657 100644 Binary files a/data/images/checkbox_checked.png and b/data/images/checkbox_checked.png differ diff --git a/data/images/checkbox_checked_old.png b/data/images/checkbox_checked_old.png new file mode 100644 index 0000000..089d1e0 Binary files /dev/null and b/data/images/checkbox_checked_old.png differ diff --git a/data/images/checkbox_unchecked.png b/data/images/checkbox_unchecked.png index 02c22e9..a8b5e2c 100644 Binary files a/data/images/checkbox_unchecked.png and b/data/images/checkbox_unchecked.png differ diff --git a/data/images/checkbox_unchecked_old.png b/data/images/checkbox_unchecked_old.png new file mode 100644 index 0000000..02c22e9 Binary files /dev/null and b/data/images/checkbox_unchecked_old.png differ diff --git a/data/images/icon-arrow-right.svg b/data/images/icon-arrow-right.svg new file mode 100644 index 0000000..580847d --- /dev/null +++ b/data/images/icon-arrow-right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/data/images/icon-camera.svg b/data/images/icon-camera.svg new file mode 100644 index 0000000..cb33119 --- /dev/null +++ b/data/images/icon-camera.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/data/images/icon-checkmark-circle.svg b/data/images/icon-checkmark-circle.svg new file mode 100644 index 0000000..11d9174 --- /dev/null +++ b/data/images/icon-checkmark-circle.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/data/images/icon-microphone.svg b/data/images/icon-microphone.svg new file mode 100644 index 0000000..8e6b32e --- /dev/null +++ b/data/images/icon-microphone.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/data/images/icon-scene-collection.svg b/data/images/icon-scene-collection.svg new file mode 100644 index 0000000..100467a --- /dev/null +++ b/data/images/icon-scene-collection.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/data/images/info-icon.svg b/data/images/info-icon.svg new file mode 100644 index 0000000..13d0870 --- /dev/null +++ b/data/images/info-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/data/images/marketplace-full-logo.svg b/data/images/marketplace-full-logo.svg new file mode 100644 index 0000000..81573e5 --- /dev/null +++ b/data/images/marketplace-full-logo.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/data/images/new-sc-example.png b/data/images/new-sc-example.png new file mode 100644 index 0000000..164294e Binary files /dev/null and b/data/images/new-sc-example.png differ diff --git a/data/images/stepper-marker-current.svg b/data/images/stepper-marker-current.svg new file mode 100644 index 0000000..39ba607 --- /dev/null +++ b/data/images/stepper-marker-current.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/images/stepper-marker-future.svg b/data/images/stepper-marker-future.svg new file mode 100644 index 0000000..0a2914a --- /dev/null +++ b/data/images/stepper-marker-future.svg @@ -0,0 +1,3 @@ + + + diff --git a/data/images/stepper-marker-prior.svg b/data/images/stepper-marker-prior.svg new file mode 100644 index 0000000..da4622a --- /dev/null +++ b/data/images/stepper-marker-prior.svg @@ -0,0 +1,3 @@ + + + diff --git a/data/images/stepper-separator-active.svg b/data/images/stepper-separator-active.svg new file mode 100644 index 0000000..dd3d355 --- /dev/null +++ b/data/images/stepper-separator-active.svg @@ -0,0 +1,3 @@ + + + diff --git a/data/images/stepper-separator-inactive.svg b/data/images/stepper-separator-inactive.svg new file mode 100644 index 0000000..729bea3 --- /dev/null +++ b/data/images/stepper-separator-inactive.svg @@ -0,0 +1,3 @@ + + + diff --git a/data/images/toggle-off.png b/data/images/toggle-off.png new file mode 100644 index 0000000..00f2e55 Binary files /dev/null and b/data/images/toggle-off.png differ diff --git a/data/images/toggle-on.png b/data/images/toggle-on.png new file mode 100644 index 0000000..3d9ac2f Binary files /dev/null and b/data/images/toggle-on.png differ diff --git a/data/images/your-library-icon.svg b/data/images/your-library-icon.svg new file mode 100644 index 0000000..137bd6c --- /dev/null +++ b/data/images/your-library-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/locale/da-DK.ini b/data/locale/da-DK.ini new file mode 100644 index 0000000..b67910d --- /dev/null +++ b/data/locale/da-DK.ini @@ -0,0 +1,129 @@ +AssetProtection.Message="Denne samling af scener indeholder elementer fra et køb, du har foretaget på Elgato Marketplace. Hvis du vil installere den, skal du logge ind med den konto, du brugte til at foretage købet. Du kan logge ind via Elgato Marketplace-menuen, og prøv derefter at importere igen." +ExportWizard.BackButton="Tilbage" +ExportWizard.CloseButton="OK" +ExportWizard.Complete="Eksporten af {COLLECTION_NAME} lykkedes" +ExportWizard.Export="Eksportér" +ExportWizard.ExportComplete.Text="Din samling af scener er nu klar til at blive testes eller uploadet til Marketplace" +ExportWizard.Exporting.Text="Dette kan tage et par minutter for samlinger af scener med større filer" +ExportWizard.Exporting.Title="Eksporterer pakke…" +ExportWizard.ExportTitle="Eksportér {COLLECTION_NAME}" +ExportWizard.GetStartedButton="Konfigurer" +ExportWizard.MediaFileCheck.SubtitleNone="0 mediefiler bliver inkluderet i samlingen af scener" +ExportWizard.MediaFileCheck.SubtitlePlural="Disse filer inkluderes i samlingen af scener" +ExportWizard.MediaFileCheck.SubtitleSingular="Denne fil inkluderes i samlingen af scener" +ExportWizard.MediaFileCheck.Title="Gennemfå din eksport" +ExportWizard.MediaFileCheck.TitlePlural="{COUNT} mediefiler blev fundet" +ExportWizard.MediaFileCheck.TitleSingular="1 mediefil blev fundet" +ExportWizard.NextButton="Næste" +ExportWizard.OutputScenes.Subtitle="Undertekst på outputscener" +ExportWizard.OutputScenes.Title="Vælg outputscener" +ExportWizard.RequiredPlugins.NoPluginsFound="Der blev ikke fundet nogen installerede plugins" +ExportWizard.RequiredPlugins.PluralTitle="Der blev fundet {COUNT} plugins" +ExportWizard.RequiredPlugins.SingleTitle="Der blev fundet 1 plugin" +ExportWizard.RequiredPlugins.SubTitle="Vælg plugins, som denne samling af scener kræver" +ExportWizard.StartExport.Description="Lad os først indsamle afhængigheder og konfigurere kilderne" +ExportWizard.Steps.BundleRequiredPlugins="Saml de påkrævede plugins" +ExportWizard.Steps.CollectMediaFiles="Indsaml mediefiler" +ExportWizard.Steps.GetStarted="Kom godt i gang" +ExportWizard.Steps.RenameVideoSources="Omdøb videokilder" +ExportWizard.Steps.SelectOutputScenes="Vælg outputscener" +ExportWizard.Steps.ThirdPartyRequirements="Tredjepartskrav" +ExportWizard.ThirdPartyRequirements.SubTitle="Angiv en liste med navne og URL'er for alle tredjepartskrav." +ExportWizard.ThirdPartyRequirements.Title="Tredjepartskrav" +ExportWizard.VideoSourceLabel.CurrentLabel="Nuværende" +ExportWizard.VideoSourceLabel.NewLabel="Ny" +ExportWizard.VideoSourceLabel.SubTitle="Vælg, hvordan videokilderne vises for brugere i deres liste over kilder" +ExportWizard.VideoSourceLabels.InputPlaceholder="Beskrivelsesteksten kommer til at stå her" +ExportWizard.VideoSourceLabels.NoCaptureSourcesSub="Ingen videokilder bliver inkluderet med samlingen af scener" +ExportWizard.VideoSourceLabels.NoCaptureSourcesText="Der blev ikke fundet nogen videokilder" +ExportWizard.VideoSourceLabels.Title="Omdøb videokilder" +General.CancelButton="Annuller" +General.SaveButton="Gem" +MarketplaceWindow.ConnectionError.Subtitle="Kunne ikke oprette forbindelse til serveren. (tjek din netværksforbindelse)" +MarketplaceWindow.ConnectionError.Title="Fejl med at oprette forbindelse" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Subtitle="Filen er ikke en .elgatoscene-fil" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Title="Ugyldig filtype" +MarketplaceWindow.DownloadProduct.NetworkError.Subtitle="Kunne ikke oprette forbindelse til Marketplace. Prøv igen." +MarketplaceWindow.DownloadProduct.NetworkError.Title="Netværksforbindelsesfejl" +MarketplaceWindow.Loading.Title="Indlæser dine købte produkter…" +MarketplaceWindow.LoggingIn.Subtitle="Virker det ikke? Klik på Prøv igen for at genåbne i browseren" +MarketplaceWindow.LoggingIn.Title="Fører dig til browseren for at logge ind" +MarketplaceWindow.LoggingIn.TryAgain="Prøv igen" +MarketplaceWindow.LoginButton.LogIn="Log ind" +MarketplaceWindow.LoginError.Subtitle="Der var et problem med at logge dig ind. Prøv at logge ind igen for at fortsætte." +MarketplaceWindow.LoginError.Title="Log ind" +MarketplaceWindow.LoginNeeded.Subtitle="Lad os som det første forbinde din Marketplace-konto med OBS Studio" +MarketplaceWindow.LoginNeeded.Title="Log ind for at komme i gang" +MarketplaceWindow.OpenSettingsButton.Tooltip="Åbn indstillinger" +MarketplaceWindow.Purchased.NoPurchasesSubtitle="Dine digitale aktiver fra Marketplace bliver vist her" +MarketplaceWindow.Purchased.NoPurchasesTitle="Du ejer ikke nogen produkter til samling af scener endnu" +MarketplaceWindow.Purchased.OpenMarketplaceButton="Udforsk Marketplace" +MarketplaceWindow.PurchasedTab="Din samling" +MarketplaceWindow.Settings.Advanced="Avanceret" +MarketplaceWindow.Settings.DefaultAudioDevice.Label="Mikrofon" +MarketplaceWindow.Settings.DefaultAV.Label="Inputenheder som standard" +MarketplaceWindow.Settings.DefaultVideoDevice.Label="Kamera" +MarketplaceWindow.Settings.DefaultVideoDevice.SettingsButton.Tooltip="Enhedsindstillinger for videooptagelse" +MarketplaceWindow.Settings.EnableMakerTools.Tip="Eksportér og importér manuelt samlinger af scener" +MarketplaceWindow.Settings.EnableMakerTools.Tooltip="Aktiverer eksport- og importværktøjer til personer, som vil oprette samlinger af scener til Elgato Marketplace." +MarketplaceWindow.Settings.EnableMakerTools="Aktivér Maker-værktøjer" +MarketplaceWindow.Settings.InstallLocation="Mappe til mediefiler" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Disabled="Eksport og import er ikke længere tilgængeligt, når du genstarter OBS Studio" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Enabled="Eksport og import er tilgængeligt, når du genstarter OBS Studio" +MarketplaceWindow.Settings.Title="Indstillinger" +MarketplaceWindow.StoreButton.Tooltip="Gå til Elgato Marketplace" +SceneCollectionInfo.CloseButton="Luk" +SceneCollectionInfo.Text="Download og installer de påkrævede aktiver, og genstart OBS Studio for at bruge denne kollektion" +SceneCollectionInfo.Title="Tredjepartsaktiver er påkrævet for denne samling af scener" +SceneCollectionInfo.WindowTitle="Info om samling af scener" +SetupWizard.AudioSetup.Device.Text="Vælg en mikrofon" +SetupWizard.BackButton="Tilbage" +SetupWizard.CreateCollection.NewNameLabel="Navn" +SetupWizard.CreateCollection.SubTitle="Dette navn vises i din liste over samlinger af scener" +SetupWizard.CreateCollection.Title="Navngiv samling af scener" +SetupWizard.ImportTitlePrefix="Importér" +SetupWizard.IncompatibleFile.Text="Fejl: Denne download indeholdte ikke en gyldig bundleInfo.json-fil og kan ikke installeres. (der er et problem med den indsendte fil med samling af scener på serveren)" +SetupWizard.IncompatibleFile.Title="Ikke kompatibel fil" +SetupWizard.InstallButton="Importér" +SetupWizard.Loading.Text="Dette kan tage noget tid for samlinger af scener med større filer" +SetupWizard.Loading.Title="Indlæser samling…" +SetupWizard.MergeCollection="Flet med nuværende" +SetupWizard.MergeCollectionName.SubTitle="Giv et navn til den nye, flettede samling af scener" +SetupWizard.MergeCollectionName.Title="Navngiv den flette samling af scener" +SetupWizard.MergeCollectionSteps.ChooseMicrophone="Vælg mikrofon" +SetupWizard.MergeCollectionSteps.GetStarted="Kom godt i gang" +SetupWizard.MergeCollectionSteps.SelectScenes="Vælg scener" +SetupWizard.MergeCollectionSteps.SetUpCameras="Konfigurer kameraer" +SetupWizard.MergeSelectScenes.AllScenes.Tooltip="Hvis du markerer her, bliver alle scener, som samlingen af scener indeholder, flettet i den nuværende samling" +SetupWizard.MergeSelectScenes.AllScenes="Importér alle scener" +SetupWizard.MergeSelectScenes.NoCustomMergeAvailable="Denne samling af scener tillader kun fletning af alle scener på en gang" +SetupWizard.MergeSelectScenes.SubTitle="Alle valgte scener (og afhængigheder) føjes til den nuværende samling af scener" +SetupWizard.MergeSelectScenes.Title="Vælg scener, der skal flettes" +SetupWizard.MissingPlugins.DownloadButton="Få" +SetupWizard.MissingPlugins.Text="Download og installer de påkrævede plugins, og genstart derefter OBS, og prøv igen" +SetupWizard.MissingPlugins.Title.Plural="{COUNT} plugins er påkrævet til denne samling af scener" +SetupWizard.MissingPlugins.Title.Single="1 plugin er krævet til denne samling af scener" +SetupWizard.MissingSourceClone.Description="Download og installer pluginnet, og genstart derefter OBS Studio, og prøv igen" +SetupWizard.MissingSourceClone.Title="Source Clone-pluginnet er påkrævet for at flette samlinger af scener" +SetupWizard.NameInUseError.Text="En samling af scener eksisterer allerede med dette navn. Angiv et andet navn." +SetupWizard.NameInUseError.Title="Navnet bruges allerede" +SetupWizard.NewCollection="Opret nyt" +SetupWizard.NewCollectionSteps.ChooseMicrophone="Vælg mikrofon" +SetupWizard.NewCollectionSteps.GetStarted="Kom godt i gang" +SetupWizard.NewCollectionSteps.NameSceneCollection="Navngiv samling af scener" +SetupWizard.NewCollectionSteps.SetUpCameras="Konfigurer kameraer" +SetupWizard.NextButton="Fortsæt" +SetupWizard.SelectInstall.ExistingInstallButton="Tilføj" +SetupWizard.SelectInstall.NewInstallButton="Ny" +SetupWizard.SelectInstall.Title="Hvordan vil du installere dette?" +SetupWizard.StartInstallation.Description="Lad os først navngive samlingen af scener, og vælg dine inputenheder" +SetupWizard.VideoSetup.SubTitle.Plural="Tildel kameraer til videokilderne" +SetupWizard.VideoSetup.SubTitle.Singular="Tildel et kamera til videokilden" +SetupWizard.VideoSetup.Title.Plural="{COUNT} videokilder blev fundet" +SetupWizard.VideoSetup.Title.Singular="1 videokilde blev fundet" +SetupWizard.WindowTitle="Importér samling af scener fra Elgato Marketplace" +UpdateModal.Description="Få den seneste version af Elgato Marketplace Connect for at få adgang til nye funktioner og forbedringer" +UpdateModal.DownloadUpdateButton="Opdater nu" +UpdateModal.LaterButton="Spørg mig senere" +UpdateModal.SkipVersionButton="Nej tak" +UpdateModal.Title="Opdatering tilgængelig" diff --git a/data/locale/de-DE.ini b/data/locale/de-DE.ini new file mode 100644 index 0000000..40ece0d --- /dev/null +++ b/data/locale/de-DE.ini @@ -0,0 +1,129 @@ +AssetProtection.Message="Diese Szenensammlung enthält Elemente aus einem Kauf im Elgato Marketplace. Um diese zu installieren, melde dich bitte mit dem Account an, mit dem der Kauf getätigt wurde. Du kannst dich über das Elgato Marketplace-Menü einloggen und anschließend den Import erneut versuchen." +ExportWizard.BackButton="Zurück" +ExportWizard.CloseButton="Fertig" +ExportWizard.Complete="Export von {COLLECTION_NAME} abgeschlossen" +ExportWizard.Export="Exportieren" +ExportWizard.ExportComplete.Text="Deine Szenensammlung kann jetzt getestet oder im Marketplace hochgeladen werden" +ExportWizard.Exporting.Text="Dies kann bei Szenensammlungen mit großen Dateien einige Minuten dauern" +ExportWizard.Exporting.Title="Bundle exportieren …" +ExportWizard.ExportTitle="{COLLECTION_NAME} exportieren" +ExportWizard.GetStartedButton="Einrichten" +ExportWizard.MediaFileCheck.SubtitleNone="0 Mediendateien werden in der Szenensammlung enthalten sein" +ExportWizard.MediaFileCheck.SubtitlePlural="Diese Dateien werden in der Szenensammlung enthalten sein" +ExportWizard.MediaFileCheck.SubtitleSingular="Diese Datei wird in der Szenensammlung enthalten sein" +ExportWizard.MediaFileCheck.Title="Export überprüfen" +ExportWizard.MediaFileCheck.TitlePlural="{COUNT} Mediendateien gefunden" +ExportWizard.MediaFileCheck.TitleSingular="1 Mediendatei gefunden" +ExportWizard.NextButton="Weiter" +ExportWizard.OutputScenes.Subtitle="Untertitel der Ausgabeszenen" +ExportWizard.OutputScenes.Title="Ausgabeszenen auswählen" +ExportWizard.RequiredPlugins.NoPluginsFound="Keine installierten Plugins gefunden" +ExportWizard.RequiredPlugins.PluralTitle="{COUNT} Plugins gefunden" +ExportWizard.RequiredPlugins.SingleTitle="1 Plugin gefunden" +ExportWizard.RequiredPlugins.SubTitle="Wähle für diese Szenensammlung erforderliche Plugins" +ExportWizard.StartExport.Description="Zuerst sammeln wir die Abhängigkeiten und richten die Quellen ein" +ExportWizard.Steps.BundleRequiredPlugins="Erforderliche Plugins bündeln" +ExportWizard.Steps.CollectMediaFiles="Mediendateien sammeln" +ExportWizard.Steps.GetStarted="Los geht's" +ExportWizard.Steps.RenameVideoSources="Videoquellen umbenennen" +ExportWizard.Steps.SelectOutputScenes="Ausgabeszenen auswählen" +ExportWizard.Steps.ThirdPartyRequirements="Drittanbieteranforderungen" +ExportWizard.ThirdPartyRequirements.SubTitle="Bitte nenne alle Drittanbieteranforderungen mit Name und URL." +ExportWizard.ThirdPartyRequirements.Title="Drittanbieteranforderungen" +ExportWizard.VideoSourceLabel.CurrentLabel="Aktuell" +ExportWizard.VideoSourceLabel.NewLabel="Neu" +ExportWizard.VideoSourceLabel.SubTitle="Lege fest, wie Videoquellen in den Quellenangaben der Nutzer dargestellt werden" +ExportWizard.VideoSourceLabels.InputPlaceholder="Beschreibungstext hier einfügen" +ExportWizard.VideoSourceLabels.NoCaptureSourcesSub="In der Szenensammlung werden keine Videoquellen enthalten sein" +ExportWizard.VideoSourceLabels.NoCaptureSourcesText="Keine Videoquellen gefunden" +ExportWizard.VideoSourceLabels.Title="Videoquellen umbenennen" +General.CancelButton="Abbrechen" +General.SaveButton="Speichern" +MarketplaceWindow.ConnectionError.Subtitle="Verbindung zum Server fehlgeschlagen. (Überprüfe deine Netzwerkverbindung)" +MarketplaceWindow.ConnectionError.Title="Verbindungsfehler" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Subtitle="Datei ist keine .elgatoscene-Datei" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Title="Ungültiger Dateityp" +MarketplaceWindow.DownloadProduct.NetworkError.Subtitle="Verbindung zu Marketplace fehlgeschlagen. Bitte versuche es erneut." +MarketplaceWindow.DownloadProduct.NetworkError.Title="Netzwerkverbindungsfehler" +MarketplaceWindow.Loading.Title="Gekaufte Produkte werden geladen …" +MarketplaceWindow.LoggingIn.Subtitle="Probleme? Klicke auf „Erneut versuchen“, um es im Browser neu zu laden" +MarketplaceWindow.LoggingIn.Title="Weiterleitung zum Browser zur Anmeldung …" +MarketplaceWindow.LoggingIn.TryAgain="Erneut versuchen" +MarketplaceWindow.LoginButton.LogIn="Anmelden" +MarketplaceWindow.LoginError.Subtitle="Beim Anmelden ist ein Problem aufgetreten. Bitte versuche es erneut, um fortzufahren." +MarketplaceWindow.LoginError.Title="Anmelden" +MarketplaceWindow.LoginNeeded.Subtitle="Zuerst verbinden wir deinen Marketplace-Account mit OBS Studio" +MarketplaceWindow.LoginNeeded.Title="Melde dich an, um loszulegen" +MarketplaceWindow.OpenSettingsButton.Tooltip="Einstellungen öffnen" +MarketplaceWindow.Purchased.NoPurchasesSubtitle="Deine digitalen Assets aus dem Marketplace erscheinen hier" +MarketplaceWindow.Purchased.NoPurchasesTitle="Du hast noch keine Produkte der Szenensammlung" +MarketplaceWindow.Purchased.OpenMarketplaceButton="Marketplace entdecken" +MarketplaceWindow.PurchasedTab="Deine Mediathek" +MarketplaceWindow.Settings.Advanced="Erweitert" +MarketplaceWindow.Settings.DefaultAudioDevice.Label="Mikrofon" +MarketplaceWindow.Settings.DefaultAV.Label="Standard-Eingabegeräte" +MarketplaceWindow.Settings.DefaultVideoDevice.Label="Kamera" +MarketplaceWindow.Settings.DefaultVideoDevice.SettingsButton.Tooltip="Einstellungen des Video-Aufnahmegeräts" +MarketplaceWindow.Settings.EnableMakerTools.Tip="Ex- und importiere Szenensammlungen manuell" +MarketplaceWindow.Settings.EnableMakerTools.Tooltip="Aktiviert Export- und Importwerkzeuge für Entwickler, die Szenensammlungen für den Elgato Marketplace erstellen möchten." +MarketplaceWindow.Settings.EnableMakerTools="Werkzeuge für Hersteller aktivieren" +MarketplaceWindow.Settings.InstallLocation="Mediendateien-Ordner" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Disabled="Ex- und Importieren ist nach dem Neustart von OBS Studio nicht mehr verfügbar" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Enabled="Ex- und Importieren ist nach dem Neustart von OBS Studio verfügbar" +MarketplaceWindow.Settings.Title="Einstellungen" +MarketplaceWindow.StoreButton.Tooltip="Gehe zu Elgato Marketplace" +SceneCollectionInfo.CloseButton="Schließen" +SceneCollectionInfo.Text="Lade und installiere die erforderlichen Assets und starte dann OBS Studio neu, um diese Sammlung zu nutzen" +SceneCollectionInfo.Title="Drittanbieterassets sind für diese Szenensammlung erforderlich" +SceneCollectionInfo.WindowTitle="Über die Szenensammlung" +SetupWizard.AudioSetup.Device.Text="Wähle ein Mikrofon" +SetupWizard.BackButton="Zurück" +SetupWizard.CreateCollection.NewNameLabel="Name" +SetupWizard.CreateCollection.SubTitle="Dieser Name erscheint in der Liste deiner Szenensammlungen" +SetupWizard.CreateCollection.Title="Szenensammlung benennen" +SetupWizard.ImportTitlePrefix="Importieren" +SetupWizard.IncompatibleFile.Text="Fehler: Dieser Download enthält keine gültige bundleInfo.json-Datei und kann nicht installiert werden. (Dies ist ein Problem mit der übermittelten Szenensammlungsdatei auf dem Server.)" +SetupWizard.IncompatibleFile.Title="Inkompatible Datei" +SetupWizard.InstallButton="Importieren" +SetupWizard.Loading.Text="Das kann bei Szenensammlungen mit großen Dateien etwas dauern" +SetupWizard.Loading.Title="Sammlung laden …" +SetupWizard.MergeCollection="Mit aktueller zusammenfügen" +SetupWizard.MergeCollectionName.SubTitle="Benenne die neu zusammengeführte Szenensammlung" +SetupWizard.MergeCollectionName.Title="Zusammengeführte Szenensammlung benennen" +SetupWizard.MergeCollectionSteps.ChooseMicrophone="Mikrofon auswählen" +SetupWizard.MergeCollectionSteps.GetStarted="Los geht's" +SetupWizard.MergeCollectionSteps.SelectScenes="Szenen wählen" +SetupWizard.MergeCollectionSteps.SetUpCameras="Kameras einrichten" +SetupWizard.MergeSelectScenes.AllScenes.Tooltip="Wenn aktiviert, werden alle von der Szenensammlung bereitgestellten Szenen in der aktuellen Sammlung zusammengeführt" +SetupWizard.MergeSelectScenes.AllScenes="Alle Szenen importieren" +SetupWizard.MergeSelectScenes.NoCustomMergeAvailable="Diese Szenensammlung erlaubt nur das gleichzeitige Zusammenführen aller Szenen" +SetupWizard.MergeSelectScenes.SubTitle="Alle ausgewählten Szenen (und Abhängigkeiten) werden zur aktuellen Szenensammlung hinzugefügt" +SetupWizard.MergeSelectScenes.Title="Szenen zum Zusammenfügen auswählen" +SetupWizard.MissingPlugins.DownloadButton="Laden" +SetupWizard.MissingPlugins.Text="Lade und installiere die erforderlichen Plugins, starte dann OBS neu und versuche es erneut" +SetupWizard.MissingPlugins.Title.Plural="Für diese Szenensammlung sind {COUNT} Plugins erforderlich" +SetupWizard.MissingPlugins.Title.Single="1 Plugin ist für diese Szenensammlung erforderlich" +SetupWizard.MissingSourceClone.Description="Lade und installiere das erforderliche Plugin, starte dann OBS neu und versuche es erneut" +SetupWizard.MissingSourceClone.Title="Das Source Clone-Plugin wird benötigt, um Szenensammlungen zusammenzufügen" +SetupWizard.NameInUseError.Text="Eine Szenensammlung mit diesem Namen existiert bereits. Bitte gib einen anderen Namen ein." +SetupWizard.NameInUseError.Title="Name bereits vergeben" +SetupWizard.NewCollection="Neu erstellen" +SetupWizard.NewCollectionSteps.ChooseMicrophone="Mikrofon auswählen" +SetupWizard.NewCollectionSteps.GetStarted="Los geht's" +SetupWizard.NewCollectionSteps.NameSceneCollection="Szenensammlung benennen" +SetupWizard.NewCollectionSteps.SetUpCameras="Kameras einrichten" +SetupWizard.NextButton="Weiter" +SetupWizard.SelectInstall.ExistingInstallButton="Hinzufügen" +SetupWizard.SelectInstall.NewInstallButton="Neu" +SetupWizard.SelectInstall.Title="Wie möchtest du das installieren?" +SetupWizard.StartInstallation.Description="Zuerst benennen wir die Szenensammlung und wählen deine Eingabegeräte aus" +SetupWizard.VideoSetup.SubTitle.Plural="Weise den Videoquellen Kameras zu" +SetupWizard.VideoSetup.SubTitle.Singular="Weise der Videoquelle eine Kamera zu" +SetupWizard.VideoSetup.Title.Plural="{COUNT} Videoquellen gefunden" +SetupWizard.VideoSetup.Title.Singular="1 Videoquelle gefunden" +SetupWizard.WindowTitle="Szenensammlung aus Elgato Marketplace importieren" +UpdateModal.Description="Lade die aktuelle Version von Elgato Marketplace Connect herunter, um Zugriff auf neue Features und Verbesserungen zu erhalten" +UpdateModal.DownloadUpdateButton="Jetzt aktualisieren" +UpdateModal.LaterButton="Später erinnern" +UpdateModal.SkipVersionButton="Nein, danke" +UpdateModal.Title="Update verfügbar" diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 9504920..e244768 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -1,78 +1,130 @@ -General.SaveButton="Save" -General.SaveChangesButton="Save changes" +AssetProtection.Message="This scene collection contains elements from a purchase made on the Elgato Marketplace. To install it, please sign in with the account used to make the purchase. You can log in through the Elgato Marketplace menu, then try importing again." +ExportWizard.BackButton="Back" +ExportWizard.CloseButton="Done" +ExportWizard.Complete="Export {COLLECTION_NAME} complete" +ExportWizard.Export="Export" +ExportWizard.ExportComplete.Text="Your scene collection is now ready to test or upload to Marketplace" +ExportWizard.Exporting.Text="This can take a few minutes for scene collections with large files" +ExportWizard.Exporting.Title="Exporting Bundle…" +ExportWizard.ExportTitle="Export {COLLECTION_NAME}" +ExportWizard.GetStartedButton="Set up" +ExportWizard.MediaFileCheck.SubtitleNone="0 media files will be included with the scene collection" +ExportWizard.MediaFileCheck.SubtitlePlural="These files will be included with the scene collection" +ExportWizard.MediaFileCheck.SubtitleSingular="This file will be included with the scene collection" +ExportWizard.MediaFileCheck.Title="Review your export" +ExportWizard.MediaFileCheck.TitlePlural="{COUNT} media files were found" +ExportWizard.MediaFileCheck.TitleSingular="1 media file was found" +ExportWizard.NextButton="Next" +ExportWizard.OutputScenes.Subtitle="Output Scenes Subtitle" +ExportWizard.OutputScenes.Title="Select Output Scenes" +ExportWizard.RequiredPlugins.NoPluginsFound="No installed plugins found" +ExportWizard.RequiredPlugins.PluralTitle="{COUNT} plugins were found" +ExportWizard.RequiredPlugins.SingleTitle="1 plugin was found" +ExportWizard.RequiredPlugins.SubTitle="Select plugins required for this scene collection" +ExportWizard.StartExport.Description="First, let's collect dependencies and set up sources" +ExportWizard.Steps.BundleRequiredPlugins="Bundle required plugins" +ExportWizard.Steps.CollectMediaFiles="Collect media files" +ExportWizard.Steps.GetStarted="Get started" +ExportWizard.Steps.RenameVideoSources="Rename video sources" +ExportWizard.Steps.SelectOutputScenes="Select Output Scenes" +ExportWizard.Steps.ThirdPartyRequirements="3rd Party Requirements" +ExportWizard.ThirdPartyRequirements.SubTitle="Please provide a list of names and urls for any third party requirements." +ExportWizard.ThirdPartyRequirements.Title="Third Party Requirements" +ExportWizard.VideoSourceLabel.CurrentLabel="Current" +ExportWizard.VideoSourceLabel.NewLabel="New" +ExportWizard.VideoSourceLabel.SubTitle="Set how video sources will appear for users in their list of sources" +ExportWizard.VideoSourceLabels.InputPlaceholder="Description text goes here" +ExportWizard.VideoSourceLabels.NoCaptureSourcesSub="No video sources will be included with the scene collection" +ExportWizard.VideoSourceLabels.NoCaptureSourcesText="No video sources were found" +ExportWizard.VideoSourceLabels.Title="Rename video sources" General.CancelButton="Cancel" +General.SaveButton="Save" +MarketplaceWindow.ConnectionError.Subtitle="Could not connect to the server. (please check your network connection)" +MarketplaceWindow.ConnectionError.Title="Connection error" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Subtitle="File is not an .elgatoscene file" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Title="Invalid file type" +MarketplaceWindow.DownloadProduct.NetworkError.Subtitle="Could not connect to the Marketplace. Please try again." +MarketplaceWindow.DownloadProduct.NetworkError.Title="Network connection error" +MarketplaceWindow.Loading.Title="Loading your purchased products…" +MarketplaceWindow.LoggingIn.Subtitle="Not working? Click Try again to reopen in the browser" +MarketplaceWindow.LoggingIn.Title="Taking you to the browser to sign in" +MarketplaceWindow.LoggingIn.TryAgain="Try again" MarketplaceWindow.LoginButton.LogIn="Sign in" -MarketplaceWindow.LoginButton.LogOut="Sign out" +MarketplaceWindow.LoginError.Subtitle="There was an issue signing you in. Please try signing in again to continue." +MarketplaceWindow.LoginError.Title="Sign in" +MarketplaceWindow.LoginNeeded.Subtitle="First off, let's connect your Marketplace account with OBS Studio" +MarketplaceWindow.LoginNeeded.Title="Sign in to get started" MarketplaceWindow.OpenSettingsButton.Tooltip="Open settings" -MarketplaceWindow.StoreButton.Tooltip="Go to Elgato Marketplace" -MarketplaceWindow.DownloadButton.Tooltip="Click to download" -MarketplaceWindow.PurchasedTab="Products" -MarketplaceWindow.Purchased.NoPurchasesTitle="You don't own any scene collection products yet" MarketplaceWindow.Purchased.NoPurchasesSubtitle="Your digital assets from Marketplace will appear here" +MarketplaceWindow.Purchased.NoPurchasesTitle="You don't own any scene collection products yet" MarketplaceWindow.Purchased.OpenMarketplaceButton="Explore Marketplace" -MarketplaceWindow.LoginNeeded.Title="Sign in to get started" -MarketplaceWindow.LoginNeeded.Subtitle="First off, let's connect your Marketplace account with OBS Studio" -MarketplaceWindow.LoginError.Title="Sign in" -MarketplaceWindow.LoginError.Subtitle="There was an issue signing you in. Please try signing in again to continue." -MarketplaceWindow.LoggingIn.Title="Signing you in" -MarketplaceWindow.LoggingIn.Subtitle="Use the new browser window to sign into your account, or select Try again to reopen" -MarketplaceWindow.LoggingIn.TryAgain="Try again" -MarketplaceWindow.ConnectionError.Title="Connection error" -MarketplaceWindow.ConnectionError.Subtitle="Could not connect to the server. (please check your network connection)" -MarketplaceWindow.Loading.Title="Loading your purchased products..." -MarketplaceWindow.Settings.Title="Settings" -MarketplaceWindow.Settings.DefaultVideoDevice.Label="Default camera" -MarketplaceWindow.Settings.DefaultVideoDevice.NoneSelected="None selected" +MarketplaceWindow.PurchasedTab="Your library" +MarketplaceWindow.Settings.Advanced="Advanced" +MarketplaceWindow.Settings.DefaultAudioDevice.Label="Microphone" +MarketplaceWindow.Settings.DefaultAV.Label="Default input devices" +MarketplaceWindow.Settings.DefaultVideoDevice.Label="Camera" MarketplaceWindow.Settings.DefaultVideoDevice.SettingsButton.Tooltip="Video capture device settings" -MarketplaceWindow.Settings.DefaultAudioDevice.Label="Default microphone" -MarketplaceWindow.Settings.InstallLocation="Media file install location" -MarketplaceWindow.Settings.EnableMakerTools="Turn on maker tools" +MarketplaceWindow.Settings.EnableMakerTools.Tip="Manually export and import scene collections" MarketplaceWindow.Settings.EnableMakerTools.Tooltip="Enables export and import tools for makers who want to create scene collections for the Elgato Marketplace." -MarketplaceWindow.Settings.MakerToolsRestartWarning.Disabled="Maker tools will be disabled after restarting OBS" -MarketplaceWindow.Settings.MakerToolsRestartWarning.Enabled="Maker tools will be enabled after restarting OBS" -MarketplaceWindow.DownloadProduct.NetworkError.Title="Network connection error" -MarketplaceWindow.DownloadProduct.NetworkError.Subtitle="Could not connect to the Marketplace. Please try again." -MarketplaceWindow.DownloadProduct.InvalidFiletype.Title="Invalid file type" -MarketplaceWindow.DownloadProduct.InvalidFiletype.Subtitle="File is not an .elgatoscene file" -ExportWizard.NextButton="Continue" -ExportWizard.BackButton="Back" -ExportWizard.SaveAsButton="Save as" -ExportWizard.CloseButton="Done" -ExportWizard.MediaFileCheck.Title="Review your export" -ExportWizard.MediaFileCheck.Text=" media files were found to bundle" -ExportWizard.VideoSourceLabels.Title="Name video sources" -ExportWizard.VideoSourceLabels.Text="Choose how your video sources will appear for users" -ExportWizard.VideoSourceLabels.InputPlaceholder="Description text goes here" -ExportWizard.VideoSourceLabels.NoCaptureSourcesText="No video capture sources found" -ExportWizard.RequiredPlugins.Title="Add required plugins" -ExportWizard.RequiredPlugins.Text="Select any plugins needed to run this asset" -ExportWizard.RequiredPlugins.NoPluginsFound="No installed plugins found" -ExportWizard.ExportComplete.Title="Export complete" -ExportWizard.ExportComplete.Text="Your scene collection is now ready to test or upload to Marketplace" -ExportWizard.Exporting.Title="Exporting Bundle..." -ExportWizard.Exporting.Text="This can take a few minutes for scene collections with large files" -SetupWizard.WindowTitle="Install Scene Collection" -SetupWizard.NextButton="Continue" +MarketplaceWindow.Settings.EnableMakerTools="Enable Maker tools" +MarketplaceWindow.Settings.InstallLocation="Media files folder" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Disabled="Exporting and importing will no longer be available after restarting OBS Studio" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Enabled="Exporting and importing will be available after restarting OBS Studio" +MarketplaceWindow.Settings.Title="Settings" +MarketplaceWindow.StoreButton.Tooltip="Go to Elgato Marketplace" +SceneCollectionInfo.CloseButton="Close" +SceneCollectionInfo.Text="Download and install the required assets, then restart OBS Studio to use this collection" +SceneCollectionInfo.Title="Third party assets are required for this scene collection" +SceneCollectionInfo.WindowTitle="Scene Collection Info" +SetupWizard.AudioSetup.Device.Text="Select a microphone" SetupWizard.BackButton="Back" -SetupWizard.GetStartedButton="Get started" -SetupWizard.StartInstall.Title="Just a few quick steps and\nyour setup is ready to use" -SetupWizard.MissingPlugins.Title="You're missing something" -SetupWizard.MissingPlugins.Text="To continue, download the following free plugins, then restart OBS and try again" -SetupWizard.MissingPlugins.DownloadButton="Get" -SetupWizard.SelectInstall.Title="How do you want to install this?" -SetupWizard.SelectInstall.NewInstallButton="New" -SetupWizard.SelectInstall.ExistingInstallButton="Add" +SetupWizard.CreateCollection.NewNameLabel="Name" +SetupWizard.CreateCollection.SubTitle="This name will appear in your list of scene collections" SetupWizard.CreateCollection.Title="Create scene collection" -SetupWizard.CreateCollection.NewNamePlaceholder="Collection name" -SetupWizard.NameInUseError.Title="Name in use" -SetupWizard.NameInUseError.Text="A scene collection already exists with this name. Please provide another name." -SetupWizard.AudioSetup.Device.Text="Select a microphone" -SetupWizard.Loading.Title="Loading Collection..." -SetupWizard.Loading.Text="This can take some time for scene collections with large files" -SetupWizard.IncompatibleFile.Title="Incompatible file" +SetupWizard.CreateCollection.Title="Name scene collection" +SetupWizard.ImportTitlePrefix="Import" SetupWizard.IncompatibleFile.Text="Error: this download did not contain a valid bundleInfo.json file and cannot be installed. (this is a problem with the submitted scene collection file on the server)" -UpdateModal.Title="Update available" -UpdateModal.SkipVersionButton="No thanks" -UpdateModal.LaterButton="Ask me later" +SetupWizard.IncompatibleFile.Title="Incompatible file" +SetupWizard.InstallButton="Import" +SetupWizard.Loading.Text="This can take some time for scene collections with large files" +SetupWizard.Loading.Title="Loading Collection…" +SetupWizard.MergeCollection="Merge with current" +SetupWizard.MergeCollectionName.SubTitle="Give a name for the new merged scene collection" +SetupWizard.MergeCollectionName.Title="Name merged scene collection" +SetupWizard.MergeCollectionSteps.ChooseMicrophone="Choose microphone" +SetupWizard.MergeCollectionSteps.GetStarted="Get started" +SetupWizard.MergeCollectionSteps.SelectScenes="Select scenes" +SetupWizard.MergeCollectionSteps.SetUpCameras="Set up cameras" +SetupWizard.MergeSelectScenes.AllScenes.Tooltip="If checked, all scenes provided by the scene collection will be merged into the current collection" +SetupWizard.MergeSelectScenes.AllScenes="Import all scenes" +SetupWizard.MergeSelectScenes.NoCustomMergeAvailable="This scene collection only allows merging of all scenes at once" +SetupWizard.MergeSelectScenes.SubTitle="All selected scenes (and dependencies) will be added to the current scene collection" +SetupWizard.MergeSelectScenes.Title="Select scenes to merge" +SetupWizard.MissingPlugins.DownloadButton="Get" +SetupWizard.MissingPlugins.Text="Download and install the required plugins, then restart OBS and try again" +SetupWizard.MissingPlugins.Title.Plural="{COUNT} plugins are needed for this scene collection" +SetupWizard.MissingPlugins.Title.Single="1 plugin is needed for this scene collection" +SetupWizard.MissingSourceClone.Description="Download and install the plugin, then restart OBS Studio and try again" +SetupWizard.MissingSourceClone.Title="Source Clone plugin is required to merge scene collections" +SetupWizard.NameInUseError.Text="A scene collection already exists with this name. Please provide another name." +SetupWizard.NameInUseError.Title="Name in use" +SetupWizard.NewCollection="Create new" +SetupWizard.NewCollectionSteps.ChooseMicrophone="Choose microphone" +SetupWizard.NewCollectionSteps.GetStarted="Get started" +SetupWizard.NewCollectionSteps.NameSceneCollection="Name scene collection" +SetupWizard.NewCollectionSteps.SetUpCameras="Set up cameras" +SetupWizard.NextButton="Continue" +SetupWizard.SelectInstall.ExistingInstallButton="Add" +SetupWizard.SelectInstall.NewInstallButton="New" +SetupWizard.SelectInstall.Title="How do you want to install this?" +SetupWizard.StartInstallation.Description="First, let's name the scene collection and select your input devices" +SetupWizard.VideoSetup.SubTitle.Plural="Assign cameras to the video sources" +SetupWizard.VideoSetup.SubTitle.Singular="Assign a camera to the video source" +SetupWizard.VideoSetup.Title.Plural="{COUNT} video sources found" +SetupWizard.VideoSetup.Title.Singular="1 video source found" +SetupWizard.WindowTitle="Import scene collection from Elgato Marketplace" +UpdateModal.Description="Get the latest version of Elgato Marketplace Connect to access new features and improvements" UpdateModal.DownloadUpdateButton="Update now" -UpdateModal.Description="Get the latest version of Elgato Marketplace Connect to access new features and improvements" \ No newline at end of file +UpdateModal.LaterButton="Ask me later" +UpdateModal.SkipVersionButton="No thanks" +UpdateModal.Title="Update available" \ No newline at end of file diff --git a/data/locale/es-ES.ini b/data/locale/es-ES.ini new file mode 100644 index 0000000..57a28bc --- /dev/null +++ b/data/locale/es-ES.ini @@ -0,0 +1,129 @@ +AssetProtection.Message="Esta colección de escenas contiene elementos de una compra realizada en Elgato Marketplace. Para instalarla, inicia sesión con la cuenta que usaste para la compra. Puedes iniciar sesión desde el menú de Elgato Marketplace y, a continuación, volver a intentar importarla." +ExportWizard.BackButton="Atrás" +ExportWizard.CloseButton="OK" +ExportWizard.Complete="Exportación de {COLLECTION_NAME} finalizada" +ExportWizard.Export="Exportar" +ExportWizard.ExportComplete.Text="Tu colección de escenas ya se puede probar o cargar en el Marketplace" +ExportWizard.Exporting.Text="Las colecciones de escenas con archivos grandes pueden tardar unos minutos" +ExportWizard.Exporting.Title="Exportando paquete…" +ExportWizard.ExportTitle="Exportar {COLLECTION_NAME}" +ExportWizard.GetStartedButton="Configurar" +ExportWizard.MediaFileCheck.SubtitleNone="No se incluirá ningún archivo audiovisual con la colección de escenas" +ExportWizard.MediaFileCheck.SubtitlePlural="Los siguientes archivos se incluirán con la colección de escenas" +ExportWizard.MediaFileCheck.SubtitleSingular="El siguiente archivo se incluirá con la colección de escenas" +ExportWizard.MediaFileCheck.Title="Revisa la exportación" +ExportWizard.MediaFileCheck.TitlePlural="Se han encontrado {COUNT} archivos audiovisuales" +ExportWizard.MediaFileCheck.TitleSingular="Se ha encontrado 1 archivo audiovisual" +ExportWizard.NextButton="Siguiente" +ExportWizard.OutputScenes.Subtitle="Subtítulo de escenas de salida" +ExportWizard.OutputScenes.Title="Seleccionar escenas de salida" +ExportWizard.RequiredPlugins.NoPluginsFound="No se han encontrado plugins instalados" +ExportWizard.RequiredPlugins.PluralTitle="Se han encontrado {COUNT} plugins" +ExportWizard.RequiredPlugins.SingleTitle="Se ha encontrado 1 plugin" +ExportWizard.RequiredPlugins.SubTitle="Selecciona los plugins requeridos para esta colección de escenas" +ExportWizard.StartExport.Description="Empecemos recopilando las dependencias y configurando las fuentes" +ExportWizard.Steps.BundleRequiredPlugins="Incluir plugins requeridos" +ExportWizard.Steps.CollectMediaFiles="Recopilar archivos" +ExportWizard.Steps.GetStarted="Inicio" +ExportWizard.Steps.RenameVideoSources="Renombrar fuentes de vídeo" +ExportWizard.Steps.SelectOutputScenes="Seleccionar escenas de salida" +ExportWizard.Steps.ThirdPartyRequirements="Requisitos de terceros" +ExportWizard.ThirdPartyRequirements.SubTitle="Indica un listado de nombres y URL para los requisitos necesarios de terceros." +ExportWizard.ThirdPartyRequirements.Title="Requisitos de terceros" +ExportWizard.VideoSourceLabel.CurrentLabel="Actual" +ExportWizard.VideoSourceLabel.NewLabel="Nuevo" +ExportWizard.VideoSourceLabel.SubTitle="Indica cómo aparecerán las fuentes de vídeo para los usuarios en su lista de fuentes" +ExportWizard.VideoSourceLabels.InputPlaceholder="Escribe aquí la descripción" +ExportWizard.VideoSourceLabels.NoCaptureSourcesSub="No se incluirán fuentes de vídeo con la colección de escenas" +ExportWizard.VideoSourceLabels.NoCaptureSourcesText="No se han encontrado fuentes de vídeo" +ExportWizard.VideoSourceLabels.Title="Renombrar fuentes de vídeo" +General.CancelButton="Cancelar" +General.SaveButton="Guardar" +MarketplaceWindow.ConnectionError.Subtitle="No se ha podido conectar al servidor; comprueba la conexión de red." +MarketplaceWindow.ConnectionError.Title="Error de conexión" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Subtitle="El archivo no es de tipo .elgatoscene" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Title="Tipo de archivo no válido" +MarketplaceWindow.DownloadProduct.NetworkError.Subtitle="No se ha podido conectar al Marketplace. Vuelve a intentarlo." +MarketplaceWindow.DownloadProduct.NetworkError.Title="Error de conexión de red" +MarketplaceWindow.Loading.Title="Cargando los productos que has comprado…" +MarketplaceWindow.LoggingIn.Subtitle="¿No funciona? Pulsa Volver a intentar para abrirlo en el navegador." +MarketplaceWindow.LoggingIn.Title="Abriendo el navegador para iniciar sesión" +MarketplaceWindow.LoggingIn.TryAgain="Volver a intentar" +MarketplaceWindow.LoginButton.LogIn="Iniciar sesión" +MarketplaceWindow.LoginError.Subtitle="Ha habido un problema al iniciar sesión. Vuelve a intentarlo para continuar." +MarketplaceWindow.LoginError.Title="Iniciar sesión" +MarketplaceWindow.LoginNeeded.Subtitle="Primero, conectemos tu cuenta del Marketplace a OBS Studio." +MarketplaceWindow.LoginNeeded.Title="Inicia sesión para empezar" +MarketplaceWindow.OpenSettingsButton.Tooltip="Abrir ajustes" +MarketplaceWindow.Purchased.NoPurchasesSubtitle="Tus recursos digitales del Marketplace aparecerán aquí" +MarketplaceWindow.Purchased.NoPurchasesTitle="Aún no posees ninguna colección de escenas" +MarketplaceWindow.Purchased.OpenMarketplaceButton="Explorar el Marketplace" +MarketplaceWindow.PurchasedTab="Tu biblioteca" +MarketplaceWindow.Settings.Advanced="Avanzado" +MarketplaceWindow.Settings.DefaultAudioDevice.Label="Micrófono" +MarketplaceWindow.Settings.DefaultAV.Label="Dispositivos predeterminados de entrada" +MarketplaceWindow.Settings.DefaultVideoDevice.Label="Cámara" +MarketplaceWindow.Settings.DefaultVideoDevice.SettingsButton.Tooltip="Ajustes de dispositivo de captura de vídeo" +MarketplaceWindow.Settings.EnableMakerTools.Tip="Exportar e importar colecciones de escenas a mano" +MarketplaceWindow.Settings.EnableMakerTools.Tooltip="Activa las herramientas de exportación e importación para quienes quieran crear colecciones de escenas para Elgato Marketplace." +MarketplaceWindow.Settings.EnableMakerTools="Activar herramientas de creación" +MarketplaceWindow.Settings.InstallLocation="Carpeta de archivos audiovisuales" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Disabled="La exportación e importación dejarán de estar disponibles tras reiniciar OBS Studio" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Enabled="La exportación e importación estarán disponibles tras reiniciar OBS Studio" +MarketplaceWindow.Settings.Title="Ajustes" +MarketplaceWindow.StoreButton.Tooltip="Ir a Elgato Marketplace" +SceneCollectionInfo.CloseButton="Cerrar" +SceneCollectionInfo.Text="Descarga e instala los recursos necesarios y, a continuación, reinicia OBS Studio para usar esta colección" +SceneCollectionInfo.Title="Se requieren recursos de terceros para esta colección de escenas" +SceneCollectionInfo.WindowTitle="Información de colección de escenas" +SetupWizard.AudioSetup.Device.Text="Selecciona un micrófono" +SetupWizard.BackButton="Atrás" +SetupWizard.CreateCollection.NewNameLabel="Nombre" +SetupWizard.CreateCollection.SubTitle="Este nombre aparecerá en la lista de colecciones de escenas" +SetupWizard.CreateCollection.Title="Asigna un nombre a la colección de escenas" +SetupWizard.ImportTitlePrefix="Importar" +SetupWizard.IncompatibleFile.Text="Error: esta descarga no contenía un archivo bundleInfo.json válido y no se puede instalar (se trata de un problema con el archivo de colección de escenas enviado al servidor)." +SetupWizard.IncompatibleFile.Title="Archivo incompatible" +SetupWizard.InstallButton="Importar" +SetupWizard.Loading.Text="Las colecciones de escenas con archivos grandes pueden tardar un rato" +SetupWizard.Loading.Title="Cargando colección…" +SetupWizard.MergeCollection="Combinar con la actual" +SetupWizard.MergeCollectionName.SubTitle="Asigna un nombre a la nueva colección de escenas combinada" +SetupWizard.MergeCollectionName.Title="Nombre de la colección de escenas combinada" +SetupWizard.MergeCollectionSteps.ChooseMicrophone="Seleccionar micrófono" +SetupWizard.MergeCollectionSteps.GetStarted="Primeros pasos" +SetupWizard.MergeCollectionSteps.SelectScenes="Seleccionar escenas" +SetupWizard.MergeCollectionSteps.SetUpCameras="Configurar cámaras" +SetupWizard.MergeSelectScenes.AllScenes.Tooltip="Si está activado, todas las escenas proporcionadas en la colección de escenas se combinarán en la colección actual" +SetupWizard.MergeSelectScenes.AllScenes="Importar todas las escenas" +SetupWizard.MergeSelectScenes.NoCustomMergeAvailable="Esta colección de escenas solo permite combinar todas las escenas a la vez" +SetupWizard.MergeSelectScenes.SubTitle="Todas las escenas seleccionadas (y sus dependencias) se añadirán a la colección de escenas actual" +SetupWizard.MergeSelectScenes.Title="Selecciona escenas para combinar" +SetupWizard.MissingPlugins.DownloadButton="Obtener" +SetupWizard.MissingPlugins.Text="Descarga e instala los plugins necesarios y, a continuación, reinicia OBS Studio y vuelve a intentarlo" +SetupWizard.MissingPlugins.Title.Plural="Se requieren {COUNT} plugins para esta colección de escenas" +SetupWizard.MissingPlugins.Title.Single="Se requiere 1 plugin para esta colección de escenas" +SetupWizard.MissingSourceClone.Description="Descarga e instala el plugin y, a continuación, reinicia OBS Studio y vuelve a intentarlo" +SetupWizard.MissingSourceClone.Title="Se requiere el plugin Source Clone para combinar las colecciones de escenas" +SetupWizard.NameInUseError.Text="Ya hay una colección de escenas con ese nombre. Indica un nombre distinto." +SetupWizard.NameInUseError.Title="El nombre ya existe" +SetupWizard.NewCollection="Crear nueva" +SetupWizard.NewCollectionSteps.ChooseMicrophone="Seleccionar micrófono" +SetupWizard.NewCollectionSteps.GetStarted="Primeros pasos" +SetupWizard.NewCollectionSteps.NameSceneCollection="Nombre de colección de escenas" +SetupWizard.NewCollectionSteps.SetUpCameras="Configurar cámaras" +SetupWizard.NextButton="Continuar" +SetupWizard.SelectInstall.ExistingInstallButton="Añadir" +SetupWizard.SelectInstall.NewInstallButton="Nuevo" +SetupWizard.SelectInstall.Title="¿Cómo quieres instalar esto?" +SetupWizard.StartInstallation.Description="Primero, vamos a asignarle un nombre a la colección de escenas y seleccionar los dispositivos de entrada" +SetupWizard.VideoSetup.SubTitle.Plural="Asignar cámaras a las fuentes de vídeo" +SetupWizard.VideoSetup.SubTitle.Singular="Asigna una cámara a la fuente de vídeo" +SetupWizard.VideoSetup.Title.Plural="Se han encontrado {COUNT} fuentes de vídeo" +SetupWizard.VideoSetup.Title.Singular="Se ha encontrado 1 fuente de vídeo" +SetupWizard.WindowTitle="Importar colección de escenas de Elgato Marketplace" +UpdateModal.Description="Consigue la última versión de Elgato Marketplace Connect para acceder a las nuevas funciones y mejoras" +UpdateModal.DownloadUpdateButton="Actualizar ahora" +UpdateModal.LaterButton="Volver a preguntar" +UpdateModal.SkipVersionButton="No, gracias" +UpdateModal.Title="Actualización disponible" diff --git a/data/locale/fr-FR.ini b/data/locale/fr-FR.ini new file mode 100644 index 0000000..159065b --- /dev/null +++ b/data/locale/fr-FR.ini @@ -0,0 +1,129 @@ +AssetProtection.Message="Cette collection de scènes contient des éléments qui proviennent d'un achat effectué sur Elgato Marketplace. Pour l'installer, connectez-vous au compte utilisé lors de cet achat. Vous pouvez vous connecter via le menu Elgato Marketplace, puis retenter l'importation." +ExportWizard.BackButton="Retour" +ExportWizard.CloseButton="OK" +ExportWizard.Complete="Exportation de {COLLECTION_NAME} terminée" +ExportWizard.Export="Exporter" +ExportWizard.ExportComplete.Text="Vous pouvez maintenant tester votre collection de scènes ou l'importer sur Marketplace" +ExportWizard.Exporting.Text="Pour les collections de scènes comportant des fichiers volumineux, l'opération peut prendre quelques minutes" +ExportWizard.Exporting.Title="Exportation du bundle…" +ExportWizard.ExportTitle="Exporter {COLLECTION_NAME}" +ExportWizard.GetStartedButton="Configurer" +ExportWizard.MediaFileCheck.SubtitleNone="Aucun fichier multimédia ne sera inclus dans la collection de scènes" +ExportWizard.MediaFileCheck.SubtitlePlural="Ces fichiers seront inclus dans la collection de scènes" +ExportWizard.MediaFileCheck.SubtitleSingular="Ce fichier sera inclus dans la collection de scènes" +ExportWizard.MediaFileCheck.Title="Vérifier votre exportation" +ExportWizard.MediaFileCheck.TitlePlural="{COUNT} fichiers multimédias ont été détectés" +ExportWizard.MediaFileCheck.TitleSingular="1 fichier multimédia a été détecté" +ExportWizard.NextButton="Suivant" +ExportWizard.OutputScenes.Subtitle="Sous-titre des scènes de sortie" +ExportWizard.OutputScenes.Title="Sélectionner les scènes de sortie" +ExportWizard.RequiredPlugins.NoPluginsFound="Aucun plug-in installé détecté" +ExportWizard.RequiredPlugins.PluralTitle="{COUNT} plug-ins ont été détectés" +ExportWizard.RequiredPlugins.SingleTitle="1 plug-in a été détecté" +ExportWizard.RequiredPlugins.SubTitle="Sélectionnez les plug-ins requis pour cette collection de scènes" +ExportWizard.StartExport.Description="Commençons par collecter les dépendances et par configurer les sources" +ExportWizard.Steps.BundleRequiredPlugins="Plug-ins requis par le bundle" +ExportWizard.Steps.CollectMediaFiles="Collecter les fichiers multimédias" +ExportWizard.Steps.GetStarted="Commencer" +ExportWizard.Steps.RenameVideoSources="Renommer les sources vidéo" +ExportWizard.Steps.SelectOutputScenes="Sélectionner les scènes de sortie" +ExportWizard.Steps.ThirdPartyRequirements="Exigences tierces" +ExportWizard.ThirdPartyRequirements.SubTitle="Veuillez fournir la liste des noms et les URL des éventuelles exigences tierces." +ExportWizard.ThirdPartyRequirements.Title="Exigences tierces" +ExportWizard.VideoSourceLabel.CurrentLabel="Nom actuel" +ExportWizard.VideoSourceLabel.NewLabel="Nouveau nom" +ExportWizard.VideoSourceLabel.SubTitle="Choisissez le nom des sources vidéo que les utilisateurs verront dans leur liste de sources" +ExportWizard.VideoSourceLabels.InputPlaceholder="Ajouter ici le texte de description" +ExportWizard.VideoSourceLabels.NoCaptureSourcesSub="Aucune source vidéo ne sera incluse dans la collection de scènes" +ExportWizard.VideoSourceLabels.NoCaptureSourcesText="Aucune source vidéo n'a été détectée" +ExportWizard.VideoSourceLabels.Title="Renommer les sources vidéo" +General.CancelButton="Annuler" +General.SaveButton="Enregistrer" +MarketplaceWindow.ConnectionError.Subtitle="Impossible de se connecter au serveur (veuillez vérifier votre connexion réseau)." +MarketplaceWindow.ConnectionError.Title="Erreur de connexion" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Subtitle="Le fichier n'est pas un fichier .elgatoscene" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Title="Type de fichier non valide" +MarketplaceWindow.DownloadProduct.NetworkError.Subtitle="Impossible de se connecter à Marketplace. Veuillez réessayer." +MarketplaceWindow.DownloadProduct.NetworkError.Title="Erreur de connexion réseau" +MarketplaceWindow.Loading.Title="Chargement de vos produits achetés…" +MarketplaceWindow.LoggingIn.Subtitle="Cela ne fonctionne pas ? Cliquez sur Réessayer pour rouvrir le lien dans le navigateur" +MarketplaceWindow.LoggingIn.Title="Redirection vers le navigateur en vue de la connexion…" +MarketplaceWindow.LoggingIn.TryAgain="Réessayer" +MarketplaceWindow.LoginButton.LogIn="Se connecter" +MarketplaceWindow.LoginError.Subtitle="Un problème est survenu lors de la connexion. Pour continuer, veuillez réessayer de vous connecter." +MarketplaceWindow.LoginError.Title="Se connecter" +MarketplaceWindow.LoginNeeded.Subtitle="Commençons par connecter votre compte Marketplace avec OBS Studio" +MarketplaceWindow.LoginNeeded.Title="Connectez-vous pour commencer" +MarketplaceWindow.OpenSettingsButton.Tooltip="Ouvrir les paramètres" +MarketplaceWindow.Purchased.NoPurchasesSubtitle="Vos ressources numériques issues de Marketplace apparaîtront ici" +MarketplaceWindow.Purchased.NoPurchasesTitle="Vous ne possédez aucun produit de collection de scènes pour l'instant" +MarketplaceWindow.Purchased.OpenMarketplaceButton="Explorer Marketplace" +MarketplaceWindow.PurchasedTab="Votre bibliothèque" +MarketplaceWindow.Settings.Advanced="Avancé" +MarketplaceWindow.Settings.DefaultAudioDevice.Label="Micro" +MarketplaceWindow.Settings.DefaultAV.Label="Appareils d'entrée par défaut" +MarketplaceWindow.Settings.DefaultVideoDevice.Label="Caméra" +MarketplaceWindow.Settings.DefaultVideoDevice.SettingsButton.Tooltip="Paramètres de l'appareil de capture vidéo" +MarketplaceWindow.Settings.EnableMakerTools.Tip="Exportez et importez manuellement les collections de scènes" +MarketplaceWindow.Settings.EnableMakerTools.Tooltip="Active les outils d'exportation et d'importation pour les créateurs qui veulent créer des collections de scènes pour Elgato Marketplace." +MarketplaceWindow.Settings.EnableMakerTools="Activer les outils pour les créateurs" +MarketplaceWindow.Settings.InstallLocation="Dossier des fichiers multimédias" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Disabled="Les fonctionnalités d'exportation et d'importation ne seront plus disponibles après le redémarrage d'OBS Studio" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Enabled="Les fonctionnalités d'exportation et d'importation seront disponibles après le redémarrage d'OBS Studio" +MarketplaceWindow.Settings.Title="Paramètres" +MarketplaceWindow.StoreButton.Tooltip="Accéder à Elgato Marketplace" +SceneCollectionInfo.CloseButton="Fermer" +SceneCollectionInfo.Text="Téléchargez et installez les ressources requises, puis redémarrez OBS Studio pour utiliser cette collection" +SceneCollectionInfo.Title="Cette collection de scènes nécessite des ressources tierces" +SceneCollectionInfo.WindowTitle="Infos sur la collection de scènes" +SetupWizard.AudioSetup.Device.Text="Sélectionner un micro" +SetupWizard.BackButton="Retour" +SetupWizard.CreateCollection.NewNameLabel="Nom" +SetupWizard.CreateCollection.SubTitle="Ce nom figurera dans votre liste de collections de scènes" +SetupWizard.CreateCollection.Title="Attribuer un nom à la collection de scènes" +SetupWizard.ImportTitlePrefix="Importer" +SetupWizard.IncompatibleFile.Text="Erreur : ce téléchargement ne contient pas de fichier bundleInfo.json valide et ne peut pas être installé. (Le problème est lié au fichier de collection de scènes envoyé au serveur)" +SetupWizard.IncompatibleFile.Title="Fichier non compatible" +SetupWizard.InstallButton="Importer" +SetupWizard.Loading.Text="Pour les collections de scènes comportant des fichiers volumineux, l'opération peut prendre du temps" +SetupWizard.Loading.Title="Chargement de la collection…" +SetupWizard.MergeCollection="Fusionner avec la collection actuelle" +SetupWizard.MergeCollectionName.SubTitle="Attribuez un nom à la nouvelle collection de scènes fusionnée" +SetupWizard.MergeCollectionName.Title="Attribuer un nom à la collection de scènes fusionnée" +SetupWizard.MergeCollectionSteps.ChooseMicrophone="Choisir le micro" +SetupWizard.MergeCollectionSteps.GetStarted="Commencer" +SetupWizard.MergeCollectionSteps.SelectScenes="Sélectionner des scènes" +SetupWizard.MergeCollectionSteps.SetUpCameras="Configurer les caméras" +SetupWizard.MergeSelectScenes.AllScenes.Tooltip="Si cette option est activée, toutes les scènes disponibles dans la collection de scènes seront fusionnées dans la collection actuelle" +SetupWizard.MergeSelectScenes.AllScenes="Importer toutes les scènes" +SetupWizard.MergeSelectScenes.NoCustomMergeAvailable="Cette collection de scènes permet seulement de fusionner simultanément toutes les scènes" +SetupWizard.MergeSelectScenes.SubTitle="Toutes les scènes sélectionnées (ainsi que les dépendances) seront ajoutées à la collection de scènes actuelle" +SetupWizard.MergeSelectScenes.Title="Sélectionner les scènes à fusionner" +SetupWizard.MissingPlugins.DownloadButton="Obtenir" +SetupWizard.MissingPlugins.Text="Téléchargez et installez les plug-ins requis, puis redémarrez OBS et réessayez" +SetupWizard.MissingPlugins.Title.Plural="Cette collection de scènes nécessite {COUNT} plug-ins" +SetupWizard.MissingPlugins.Title.Single="Cette collection de scènes nécessite 1 plug-in" +SetupWizard.MissingSourceClone.Description="Téléchargez et installez le plug-in, puis redémarrez OBS Studio et réessayez" +SetupWizard.MissingSourceClone.Title="Pour fusionner des collections de scènes, vous devez disposer du plug-in Source Clone" +SetupWizard.NameInUseError.Text="Une collection de scènes portant ce nom existe déjà. Veuillez indiquer un autre nom." +SetupWizard.NameInUseError.Title="Nom utilisé" +SetupWizard.NewCollection="Créer une collection" +SetupWizard.NewCollectionSteps.ChooseMicrophone="Choisir le micro" +SetupWizard.NewCollectionSteps.GetStarted="Commencer" +SetupWizard.NewCollectionSteps.NameSceneCollection="Attribuer un nom à la collection de scènes" +SetupWizard.NewCollectionSteps.SetUpCameras="Configurer les caméras" +SetupWizard.NextButton="Continuer" +SetupWizard.SelectInstall.ExistingInstallButton="Ajouter" +SetupWizard.SelectInstall.NewInstallButton="Nouvelle installation" +SetupWizard.SelectInstall.Title="Quel mode d'installation voulez-vous utiliser ?" +SetupWizard.StartInstallation.Description="Commençons par attribuer un nom à la collection de scènes et par sélectionner vos appareils d'entrée" +SetupWizard.VideoSetup.SubTitle.Plural="Associez des caméras aux sources vidéo" +SetupWizard.VideoSetup.SubTitle.Singular="Associez une caméra à la source vidéo" +SetupWizard.VideoSetup.Title.Plural="{COUNT} sources vidéo détectées" +SetupWizard.VideoSetup.Title.Singular="1 source vidéo détectée" +SetupWizard.WindowTitle="Importer une collection de scènes depuis Elgato Marketplace" +UpdateModal.Description="Installez la dernière version d'Elgato Marketplace Connect pour profiter des nouveautés et des améliorations" +UpdateModal.DownloadUpdateButton="Mettre à jour" +UpdateModal.LaterButton="Me demander plus tard" +UpdateModal.SkipVersionButton="Non, merci" +UpdateModal.Title="Mise à jour disponible" diff --git a/data/locale/it-IT.ini b/data/locale/it-IT.ini new file mode 100644 index 0000000..0fb8d00 --- /dev/null +++ b/data/locale/it-IT.ini @@ -0,0 +1,129 @@ +AssetProtection.Message="Questa raccolta di scene contiene elementi da un acquisto fatto su Elgato Marketplace. Per installarla, accedi con l'account utilizzato per fare l'acquisto. Puoi accedere dal menu di Elgato Marketplace, quindi ritentare l'importazione." +ExportWizard.BackButton="Indietro" +ExportWizard.CloseButton="Fine" +ExportWizard.Complete="Esportazione di {COLLECTION_NAME} completata" +ExportWizard.Export="Esporta" +ExportWizard.ExportComplete.Text="La tua raccolta di scene è ora pronta per essere testata o caricata su Marketplace" +ExportWizard.Exporting.Text="L'operazione potrebbe richiedere alcuni minuti per raccolte di scene con grandi file" +ExportWizard.Exporting.Title="Esportazione bundle in corso…" +ExportWizard.ExportTitle="Esporta {COLLECTION_NAME}" +ExportWizard.GetStartedButton="Imposta" +ExportWizard.MediaFileCheck.SubtitleNone="0 file multimediali verranno inclusi nella raccolta di scene" +ExportWizard.MediaFileCheck.SubtitlePlural="Questi file verranno inclusi nella raccolta di scene" +ExportWizard.MediaFileCheck.SubtitleSingular="Questo file verrà incluso nella raccolta di scene" +ExportWizard.MediaFileCheck.Title="Rivedi la tua esportazione" +ExportWizard.MediaFileCheck.TitlePlural="{COUNT} file multimediali trovati" +ExportWizard.MediaFileCheck.TitleSingular="1 file multimediale trovato" +ExportWizard.NextButton="Avanti" +ExportWizard.OutputScenes.Subtitle="Sottotitolo delle scene di output" +ExportWizard.OutputScenes.Title="Seleziona scene di output" +ExportWizard.RequiredPlugins.NoPluginsFound="Nessun plug-in installato trovato" +ExportWizard.RequiredPlugins.PluralTitle="{COUNT} plug-in trovati" +ExportWizard.RequiredPlugins.SingleTitle="1 plug-in trovato" +ExportWizard.RequiredPlugins.SubTitle="Seleziona i plug-in necessari per questa raccolta di scene" +ExportWizard.StartExport.Description="Innanzi tutto, raccogli le dipendenze e imposta le sorgenti" +ExportWizard.Steps.BundleRequiredPlugins="Plug-in necessari per il bundle" +ExportWizard.Steps.CollectMediaFiles="Raccogli file multimediali" +ExportWizard.Steps.GetStarted="Per iniziare" +ExportWizard.Steps.RenameVideoSources="Rinomina sorgenti video" +ExportWizard.Steps.SelectOutputScenes="Seleziona scene di output" +ExportWizard.Steps.ThirdPartyRequirements="Requisiti di terze parti" +ExportWizard.ThirdPartyRequirements.SubTitle="Fornisci un elenco di nomi e URL per i requisiti di terze parti." +ExportWizard.ThirdPartyRequirements.Title="Requisiti di terze parti" +ExportWizard.VideoSourceLabel.CurrentLabel="Attuale" +ExportWizard.VideoSourceLabel.NewLabel="Nuovo" +ExportWizard.VideoSourceLabel.SubTitle="Imposta come vengono visualizzate le sorgenti video nell'elenco delle sorgenti degli utenti" +ExportWizard.VideoSourceLabels.InputPlaceholder="Inserisci qui il testo della descrizione" +ExportWizard.VideoSourceLabels.NoCaptureSourcesSub="Nessuna sorgente video verrà inclusa nella raccolta di scene" +ExportWizard.VideoSourceLabels.NoCaptureSourcesText="Nessuna sorgente video trovata" +ExportWizard.VideoSourceLabels.Title="Rinomina sorgenti video" +General.CancelButton="Annulla" +General.SaveButton="Salva" +MarketplaceWindow.ConnectionError.Subtitle="Impossibile connettersi al server. (Verifica la tua connessione alla rete)" +MarketplaceWindow.ConnectionError.Title="Errore di connessione" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Subtitle="Il file non è un file .elgatoscene" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Title="Tipo di file non valido" +MarketplaceWindow.DownloadProduct.NetworkError.Subtitle="Impossibile connettersi al Marketplace. Riprova più tardi." +MarketplaceWindow.DownloadProduct.NetworkError.Title="Errore di connessione rete" +MarketplaceWindow.Loading.Title="Caricamento dei prodotti acquistati in corso…" +MarketplaceWindow.LoggingIn.Subtitle="Non funziona? Fai clic su Riprova per riaprire il browser" +MarketplaceWindow.LoggingIn.Title="Ti stiamo indirizzando al browser per accedere" +MarketplaceWindow.LoggingIn.TryAgain="Riprova" +MarketplaceWindow.LoginButton.LogIn="Accedi" +MarketplaceWindow.LoginError.Subtitle="Si è verificato un problema durante l'accesso. Riprova ad accedere per continuare." +MarketplaceWindow.LoginError.Title="Accedi" +MarketplaceWindow.LoginNeeded.Subtitle="Per prima cosa, connetti il tuo account Marketplace con OBS Studio" +MarketplaceWindow.LoginNeeded.Title="Accedi per iniziare" +MarketplaceWindow.OpenSettingsButton.Tooltip="Apri le impostazioni" +MarketplaceWindow.Purchased.NoPurchasesSubtitle="Le tue risorse digitali del Marketplace verranno visualizzate qui" +MarketplaceWindow.Purchased.NoPurchasesTitle="Non possiedi ancora alcun prodotto della raccolta di scene" +MarketplaceWindow.Purchased.OpenMarketplaceButton="Scopri il Marketplace" +MarketplaceWindow.PurchasedTab="La tua libreria" +MarketplaceWindow.Settings.Advanced="Avanzate" +MarketplaceWindow.Settings.DefaultAudioDevice.Label="Microfono" +MarketplaceWindow.Settings.DefaultAV.Label="Dispositivo di input predefinito" +MarketplaceWindow.Settings.DefaultVideoDevice.Label="Fotocamera" +MarketplaceWindow.Settings.DefaultVideoDevice.SettingsButton.Tooltip="Impostazioni dispositivo di acquisizione video" +MarketplaceWindow.Settings.EnableMakerTools.Tip="Esporta e importa manualmente le raccolte di scene" +MarketplaceWindow.Settings.EnableMakerTools.Tooltip="Abilita gli strumenti di esportazione e importazione per i creatori che desiderano creare raccolte di scene per Elgato Marketplace." +MarketplaceWindow.Settings.EnableMakerTools="Abilita strumenti per creatori" +MarketplaceWindow.Settings.InstallLocation="Cartella file multimediali" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Disabled="L'esportazione e l'importazione non saranno più disponibili dopo il riavvio di OBS Studio" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Enabled="L'esportazione e l'importazione saranno disponibili dopo il riavvio di OBS Studio" +MarketplaceWindow.Settings.Title="Impostazioni" +MarketplaceWindow.StoreButton.Tooltip="Vai a Elgato Marketplace" +SceneCollectionInfo.CloseButton="Chiudi" +SceneCollectionInfo.Text="Scarica e installa le risorse necessarie, quindi riavvia OBS per usare questa raccolta" +SceneCollectionInfo.Title="Per questa raccolta di scene sono necessarie risorse di terze parti" +SceneCollectionInfo.WindowTitle="Informazioni sulla raccolta di scene" +SetupWizard.AudioSetup.Device.Text="Seleziona un microfono" +SetupWizard.BackButton="Indietro" +SetupWizard.CreateCollection.NewNameLabel="Nome" +SetupWizard.CreateCollection.SubTitle="Questo nome verrà visualizzato nell'elenco delle raccolte di scene" +SetupWizard.CreateCollection.Title="Assegna un nome alla raccolta di scene" +SetupWizard.ImportTitlePrefix="Importa" +SetupWizard.IncompatibleFile.Text="Errore: questo download non contiene un file bundleInfo.json valido e non può essere installato. (Si tratta di un problema con il file di raccolta di scene inviato al server)" +SetupWizard.IncompatibleFile.Title="File incompatibile" +SetupWizard.InstallButton="Importa" +SetupWizard.Loading.Text="L'operazione potrebbe richiedere tempo per raccolte di scene con grandi file" +SetupWizard.Loading.Title="Caricamento raccolta in corso…" +SetupWizard.MergeCollection="Unisci con attuale" +SetupWizard.MergeCollectionName.SubTitle="Assegna un nome alla nuova raccolta di scene unita" +SetupWizard.MergeCollectionName.Title="Assegna un nome alla raccolta di scene unita" +SetupWizard.MergeCollectionSteps.ChooseMicrophone="Seleziona microfono" +SetupWizard.MergeCollectionSteps.GetStarted="Per iniziare" +SetupWizard.MergeCollectionSteps.SelectScenes="Seleziona scene" +SetupWizard.MergeCollectionSteps.SetUpCameras="Imposta le fotocamere" +SetupWizard.MergeSelectScenes.AllScenes.Tooltip="Se l'opzione è selezionata, tutte le scene fornite dalla raccolta di scene verranno unite alla raccolta attuale" +SetupWizard.MergeSelectScenes.AllScenes="Importa tutte le scene" +SetupWizard.MergeSelectScenes.NoCustomMergeAvailable="Questa raccolta di scene permette di unire tutte le scene contemporaneamente" +SetupWizard.MergeSelectScenes.SubTitle="Tutte le scene (e le dipendenze) selezionate verranno aggiunte alla raccolta di scene attuale" +SetupWizard.MergeSelectScenes.Title="Seleziona le scene da unire" +SetupWizard.MissingPlugins.DownloadButton="Ottieni" +SetupWizard.MissingPlugins.Text="Scarica e installa i plug-in necessari, quindi riavvia OBS e riprova" +SetupWizard.MissingPlugins.Title.Plural="{COUNT} plug-in necessari per questa raccolta di scene" +SetupWizard.MissingPlugins.Title.Single="È necessario 1 plug-in per questa raccolta di scene" +SetupWizard.MissingSourceClone.Description="Scarica e installa il plug-in, quindi riavvia OBS e riprova" +SetupWizard.MissingSourceClone.Title="È richiesto il plug-in Source Clone per unire le raccolte di scene" +SetupWizard.NameInUseError.Text="Esiste già una raccolta di scene con questo nome. Fornisci un altro nome." +SetupWizard.NameInUseError.Title="Nome già utilizzato" +SetupWizard.NewCollection="Crea nuovo" +SetupWizard.NewCollectionSteps.ChooseMicrophone="Seleziona microfono" +SetupWizard.NewCollectionSteps.GetStarted="Per iniziare" +SetupWizard.NewCollectionSteps.NameSceneCollection="Assegna un nome alla raccolta di scene" +SetupWizard.NewCollectionSteps.SetUpCameras="Imposta le fotocamere" +SetupWizard.NextButton="Continua" +SetupWizard.SelectInstall.ExistingInstallButton="Aggiungi" +SetupWizard.SelectInstall.NewInstallButton="Nuovo" +SetupWizard.SelectInstall.Title="Come lo vuoi installare?" +SetupWizard.StartInstallation.Description="Innanzi tutto, assegna un nome alla raccolta di scene e seleziona i dispositivi di input" +SetupWizard.VideoSetup.SubTitle.Plural="Assegna fotocamere alle sorgenti video" +SetupWizard.VideoSetup.SubTitle.Singular="Assegna una fotocamera alla sorgente video" +SetupWizard.VideoSetup.Title.Plural="{COUNT} sorgenti video trovate" +SetupWizard.VideoSetup.Title.Singular="1 sorgente video trovata" +SetupWizard.WindowTitle="Importa la raccolta di scene da Elgato Marketplace" +UpdateModal.Description="Scarica l'ultima versione di Elgato Marketplace Connect per accedere a nuove funzioni e miglioramenti" +UpdateModal.DownloadUpdateButton="Aggiorna ora" +UpdateModal.LaterButton="Richiedi in seguito" +UpdateModal.SkipVersionButton="No, grazie" +UpdateModal.Title="Aggiornamento disponibile" diff --git a/data/locale/ja-JP.ini b/data/locale/ja-JP.ini new file mode 100644 index 0000000..bc2013e --- /dev/null +++ b/data/locale/ja-JP.ini @@ -0,0 +1,129 @@ +AssetProtection.Message="このシーンコレクションにはElgato Marketplaceで購入したエレメントが含まれています。それをインストールするには、購入時に使用したアカウントでサインインする必要があります。Elgato Marketplaceのメニューからログインして、もう一度インポートしてみてください。" +ExportWizard.BackButton="戻る" +ExportWizard.CloseButton="完了" +ExportWizard.Complete="{COLLECTION_NAME}のエクスポートを完了" +ExportWizard.Export="エクスポート" +ExportWizard.ExportComplete.Text="これでシーンコレクションのテスト、およびMarketplaceへのアップロードの準備ができました" +ExportWizard.Exporting.Text="大きなファイルを含むシーンコレクションの場合、この操作に数分かかることがあります" +ExportWizard.Exporting.Title="バンドルをエクスポート中…" +ExportWizard.ExportTitle="{COLLECTION_NAME}をエクスポート" +ExportWizard.GetStartedButton="設定" +ExportWizard.MediaFileCheck.SubtitleNone="シーンコレクションに含まれるメディアファイルはありません" +ExportWizard.MediaFileCheck.SubtitlePlural="これらのファイルはシーンコレクションに含まれます" +ExportWizard.MediaFileCheck.SubtitleSingular="このファイルはシーンコレクションに含まれます" +ExportWizard.MediaFileCheck.Title="エクスポートを確認" +ExportWizard.MediaFileCheck.TitlePlural="{COUNT} 件のメディアファイルが見つかりました" +ExportWizard.MediaFileCheck.TitleSingular="1 件のメディアファイルが見つかりました" +ExportWizard.NextButton="次へ" +ExportWizard.OutputScenes.Subtitle="出力シーンの字幕" +ExportWizard.OutputScenes.Title="出力シーンを選択します" +ExportWizard.RequiredPlugins.NoPluginsFound="インストールされたプラグインが見つかりません" +ExportWizard.RequiredPlugins.PluralTitle="{COUNT} 件のプラグインが見つかりました" +ExportWizard.RequiredPlugins.SingleTitle="1 件のプラグインが見つかりました" +ExportWizard.RequiredPlugins.SubTitle="このシーンコレクションに必要なプラグインを選択してください" +ExportWizard.StartExport.Description="まず、依存関係を収集してソースを設定しましょう" +ExportWizard.Steps.BundleRequiredPlugins="必要なプラグインをバンドルします" +ExportWizard.Steps.CollectMediaFiles="メディアファイルを収集します" +ExportWizard.Steps.GetStarted="始めましょう" +ExportWizard.Steps.RenameVideoSources="ビデオソースの名前を変更します" +ExportWizard.Steps.SelectOutputScenes="出力シーンを選択します" +ExportWizard.Steps.ThirdPartyRequirements="サードパーティ要件" +ExportWizard.ThirdPartyRequirements.SubTitle="すべてのサードパーティ要件の名前とURLのリストを提供してください。" +ExportWizard.ThirdPartyRequirements.Title="サードパーティ要件" +ExportWizard.VideoSourceLabel.CurrentLabel="現在" +ExportWizard.VideoSourceLabel.NewLabel="新規" +ExportWizard.VideoSourceLabel.SubTitle="ユーザーのソース一覧でのビデオソースの表示方法を設定します" +ExportWizard.VideoSourceLabels.InputPlaceholder="説明文はこちらに表示されます" +ExportWizard.VideoSourceLabels.NoCaptureSourcesSub="ビデオソースはシーンコレクションに含まれません" +ExportWizard.VideoSourceLabels.NoCaptureSourcesText="ビデオソースが見つかりません" +ExportWizard.VideoSourceLabels.Title="ビデオソースの名前を変更します" +General.CancelButton="キャンセル" +General.SaveButton="保存" +MarketplaceWindow.ConnectionError.Subtitle="サーバに接続できませんでした (ネットワーク接続を確認してください)。" +MarketplaceWindow.ConnectionError.Title="接続エラー" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Subtitle="ファイルが .elgatoscene ファイルではありません" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Title="無効なファイルタイプです" +MarketplaceWindow.DownloadProduct.NetworkError.Subtitle="Marketplaceに接続できませんでした。やり直してください。" +MarketplaceWindow.DownloadProduct.NetworkError.Title="ネットワーク接続エラー" +MarketplaceWindow.Loading.Title="購入した製品をロード中…" +MarketplaceWindow.LoggingIn.Subtitle="うまく動作しませんか?“再試行”をクリックしてブラウザで再度開いてください" +MarketplaceWindow.LoggingIn.Title="サインイン用のブラウザに移動します" +MarketplaceWindow.LoggingIn.TryAgain="再試行" +MarketplaceWindow.LoginButton.LogIn="サインイン" +MarketplaceWindow.LoginError.Subtitle="サインインの処理に問題が起きました。続けるにはサインインし直してください。" +MarketplaceWindow.LoginError.Title="サインイン" +MarketplaceWindow.LoginNeeded.Subtitle="まず最初に、MarketplaceアカウントをOBS Studioと接続しましょう" +MarketplaceWindow.LoginNeeded.Title="サインインして始めましょう" +MarketplaceWindow.OpenSettingsButton.Tooltip="設定を開く" +MarketplaceWindow.Purchased.NoPurchasesSubtitle="Marketplaceからのデジタルアセットはこちらに表示されます" +MarketplaceWindow.Purchased.NoPurchasesTitle="まだシーンコレクション用の製品がありません" +MarketplaceWindow.Purchased.OpenMarketplaceButton="Marketplaceを探検する" +MarketplaceWindow.PurchasedTab="自分のライブラリ" +MarketplaceWindow.Settings.Advanced="詳細" +MarketplaceWindow.Settings.DefaultAudioDevice.Label="マイク" +MarketplaceWindow.Settings.DefaultAV.Label="デフォルト入力デバイス" +MarketplaceWindow.Settings.DefaultVideoDevice.Label="カメラ" +MarketplaceWindow.Settings.DefaultVideoDevice.SettingsButton.Tooltip="ビデオキャプチャデバイス設定" +MarketplaceWindow.Settings.EnableMakerTools.Tip="シーンコレクションを手動でエクスポートおよびインポートします" +MarketplaceWindow.Settings.EnableMakerTools.Tooltip="Elgato Marketplace向けにシーンコレクションを作成したいメーカーのために、エクスポートおよびインポートツールを有効にします。" +MarketplaceWindow.Settings.EnableMakerTools="メーカーツールを有効にする" +MarketplaceWindow.Settings.InstallLocation="メディアファイルフォルダ" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Disabled="OBS Studioを再開後は、エクスポートおよびインポートを利用できなくなります" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Enabled="OBS Studioを再起動後、エクスポートおよびインポートを利用できます" +MarketplaceWindow.Settings.Title="設定" +MarketplaceWindow.StoreButton.Tooltip="Elgato Marketplaceに進む" +SceneCollectionInfo.CloseButton="閉じる" +SceneCollectionInfo.Text="必要なアセットをダウンロードしてインストールしてから、OBS Studioを再起動すると、このコレクションを使用できます" +SceneCollectionInfo.Title="このシーンコレクションにはサードパーティのアセットが必要です" +SceneCollectionInfo.WindowTitle="シーンコレクションの情報" +SetupWizard.AudioSetup.Device.Text="マイクを選択します" +SetupWizard.BackButton="戻る" +SetupWizard.CreateCollection.NewNameLabel="名前" +SetupWizard.CreateCollection.SubTitle="この名前がシーンコレクションのリストに表示されます" +SetupWizard.CreateCollection.Title="シーンコレクションに名前をつける" +SetupWizard.ImportTitlePrefix="インポート" +SetupWizard.IncompatibleFile.Text="エラー: このダウンロードには有効な bundleInfo.json ファイルが含まれていないので、インストールできません (これはサーバに送信されたシーンコレクションのファイルの問題です)。" +SetupWizard.IncompatibleFile.Title="互換性のないファイルです" +SetupWizard.InstallButton="読み込む" +SetupWizard.Loading.Text="大きなファイルを含むシーンコレクションの場合、この操作に少し時間がかかることがあります" +SetupWizard.Loading.Title="コレクションをロード中…" +SetupWizard.MergeCollection="現在のものと結合" +SetupWizard.MergeCollectionName.SubTitle="新しく結合シーンコレクションに名前をつけてください" +SetupWizard.MergeCollectionName.Title="結合したシーンコレクションに名前をつける" +SetupWizard.MergeCollectionSteps.ChooseMicrophone="マイクを選択します" +SetupWizard.MergeCollectionSteps.GetStarted="始めましょう" +SetupWizard.MergeCollectionSteps.SelectScenes="シーンを選択します" +SetupWizard.MergeCollectionSteps.SetUpCameras="カメラを設定します" +SetupWizard.MergeSelectScenes.AllScenes.Tooltip="チェックすると、シーンコレクションから提供されたすべてのシーンは、現在のコレクションに結合されます" +SetupWizard.MergeSelectScenes.AllScenes="すべてのシーンをインポート" +SetupWizard.MergeSelectScenes.NoCustomMergeAvailable="このシーンコレクションはすべてのシーンの結合を1度だけ許可します" +SetupWizard.MergeSelectScenes.SubTitle="選択されたすべてのシーン (およびその依存関係) が、現在のシーンコレクションに追加されます" +SetupWizard.MergeSelectScenes.Title="統合するシーンを選択" +SetupWizard.MissingPlugins.DownloadButton="入手" +SetupWizard.MissingPlugins.Text="必要なプラグインをダウンロードしてインストールしてから、OBS Studioを再起動し、やり直してください" +SetupWizard.MissingPlugins.Title.Plural="このシーンコレクションには{COUNT} 件のプラグインが必要です" +SetupWizard.MissingPlugins.Title.Single="このシーンコレクションには1 件のプラグインが必要です" +SetupWizard.MissingSourceClone.Description="プラグインをダウンロードしてインストールしてから、OBS Studioを再起動し、やり直してください" +SetupWizard.MissingSourceClone.Title="シーンコレクションを結合するには、Source Cloneプラグインが必要です" +SetupWizard.NameInUseError.Text="この名前のシーンコレクションはすでに存在します。別の名前を入力してください。" +SetupWizard.NameInUseError.Title="名前はすでに使用されています" +SetupWizard.NewCollection="新規を作成" +SetupWizard.NewCollectionSteps.ChooseMicrophone="マイクを選択します" +SetupWizard.NewCollectionSteps.GetStarted="始めましょう" +SetupWizard.NewCollectionSteps.NameSceneCollection="シーンコレクションに名前をつける" +SetupWizard.NewCollectionSteps.SetUpCameras="カメラを設定します" +SetupWizard.NextButton="続ける" +SetupWizard.SelectInstall.ExistingInstallButton="追加" +SetupWizard.SelectInstall.NewInstallButton="新規" +SetupWizard.SelectInstall.Title="これをどのようにインストールしますか?" +SetupWizard.StartInstallation.Description="最初に、シーンコレクションに名前をつけて、入力デバイスを選択しましょう" +SetupWizard.VideoSetup.SubTitle.Plural="カメラをビデオソースに割り当てます" +SetupWizard.VideoSetup.SubTitle.Singular="カメラをビデオソースに割り当てます" +SetupWizard.VideoSetup.Title.Plural="{COUNT} 件のビデオソースが見つかりました" +SetupWizard.VideoSetup.Title.Singular="1 件のビデオソースが見つかりました" +SetupWizard.WindowTitle="Elgato Marketplaceからシーンコレクションをインポート" +UpdateModal.Description="Elgato Marketplace Connectの最新バージョンを入手して、新機能や改良された機能にアクセスしましょう" +UpdateModal.DownloadUpdateButton="今すぐ更新" +UpdateModal.LaterButton="後で確認" +UpdateModal.SkipVersionButton="しない" +UpdateModal.Title="更新を利用できます" diff --git a/data/locale/ko-KR.ini b/data/locale/ko-KR.ini new file mode 100644 index 0000000..800da32 --- /dev/null +++ b/data/locale/ko-KR.ini @@ -0,0 +1,129 @@ +AssetProtection.Message="이 장면 콜렉션에는 Elgato Marketplace에서 구매한 요소가 포함되어 있습니다. 설치하려면 해당 항목을 구매할 때 사용한 계정으로 로그인하시기 바랍니다. Elgato Marketplace 메뉴를 통해 로그인 후 다시 가져오기를 시도할 수 있습니다." +ExportWizard.BackButton="뒤로" +ExportWizard.CloseButton="완료" +ExportWizard.Complete="{COLLECTION_NAME} 내보내기 완료" +ExportWizard.Export="내보내기" +ExportWizard.ExportComplete.Text="이제 장면 콜렉션을 테스트하거나 Marketplace에 업로드할 준비가 되었습니다" +ExportWizard.Exporting.Text="대용량 파일이 포함된 장면 콜렉션의 경우 몇 분 정도 소요될 수 있습니다" +ExportWizard.Exporting.Title="번들 내보내는 중…" +ExportWizard.ExportTitle="{COLLECTION_NAME} 내보내기" +ExportWizard.GetStartedButton="설정" +ExportWizard.MediaFileCheck.SubtitleNone="0개의 미디어 파일이 장면 콜렉션에 포함됩니다" +ExportWizard.MediaFileCheck.SubtitlePlural="해당 파일이 장면 콜렉션에 포함됩니다" +ExportWizard.MediaFileCheck.SubtitleSingular="이 파일은 장면 콜렉션에 포함됩니다" +ExportWizard.MediaFileCheck.Title="내보내기 검토" +ExportWizard.MediaFileCheck.TitlePlural="{COUNT}개의 미디어 파일 발견" +ExportWizard.MediaFileCheck.TitleSingular="1개의 미디어 파일 발견" +ExportWizard.NextButton="다음" +ExportWizard.OutputScenes.Subtitle="출력 장면 자막" +ExportWizard.OutputScenes.Title="출력 장면 선택" +ExportWizard.RequiredPlugins.NoPluginsFound="설치된 플러그인을 찾을 수 없음" +ExportWizard.RequiredPlugins.PluralTitle="{COUNT}개의 플러그인 발견" +ExportWizard.RequiredPlugins.SingleTitle="1개의 플러그인 발견" +ExportWizard.RequiredPlugins.SubTitle="이 장면 콜렉션에 필요한 플러그인을 선택하세요" +ExportWizard.StartExport.Description="먼저 종속돤 콘텐츠를 수집하고 소스를 설정합니다" +ExportWizard.Steps.BundleRequiredPlugins="필요한 플러그인 수집" +ExportWizard.Steps.CollectMediaFiles="미디어 파일 수집" +ExportWizard.Steps.GetStarted="시작하기" +ExportWizard.Steps.RenameVideoSources="비디오 소스 이름 변경" +ExportWizard.Steps.SelectOutputScenes="출력 장면 선택" +ExportWizard.Steps.ThirdPartyRequirements="서드파티 요구사항" +ExportWizard.ThirdPartyRequirements.SubTitle="서드파티 요구사항에 대한 이름과 URL 목록을 제공해 주시기 바랍니다." +ExportWizard.ThirdPartyRequirements.Title="서드파티 요구사항" +ExportWizard.VideoSourceLabel.CurrentLabel="현재" +ExportWizard.VideoSourceLabel.NewLabel="신규" +ExportWizard.VideoSourceLabel.SubTitle="소스 목록에 표시할 비디오 소스의 이름을 지정합니다" +ExportWizard.VideoSourceLabels.InputPlaceholder="설명 텍스트는 여기에 표시됩니다" +ExportWizard.VideoSourceLabels.NoCaptureSourcesSub="비디오 소스는 장면 콜렉션에 포함되지 않습니다" +ExportWizard.VideoSourceLabels.NoCaptureSourcesText="비디오 소스를 찾을 수 없음" +ExportWizard.VideoSourceLabels.Title="비디오 소스 이름 변경" +General.CancelButton="취소" +General.SaveButton="저장" +MarketplaceWindow.ConnectionError.Subtitle="서버에 접속할 수 없음 (네트워크 연결 상태를 확인하세요)" +MarketplaceWindow.ConnectionError.Title="접속 오류" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Subtitle="파일 형식이 .elgatoscene이 아닙니다" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Title="잘못된 파일 형식" +MarketplaceWindow.DownloadProduct.NetworkError.Subtitle="Marketplace에 연결할 수 없습니다. 다시 시도해 주세요." +MarketplaceWindow.DownloadProduct.NetworkError.Title="네트워크 접속 오류" +MarketplaceWindow.Loading.Title="구매한 제품을 불러오는 중…" +MarketplaceWindow.LoggingIn.Subtitle="작동하지 않나요? 브라우저에서 페이지를 다시 열려면 재시도를 클릭하세요" +MarketplaceWindow.LoggingIn.Title="로그인을 위한 브라우저로 이동" +MarketplaceWindow.LoggingIn.TryAgain="재시도" +MarketplaceWindow.LoginButton.LogIn="로그인" +MarketplaceWindow.LoginError.Subtitle="로그인하는 동안 문제가 발생했습니다. 계속하려면 다시 로그인해 주세요." +MarketplaceWindow.LoginError.Title="로그인" +MarketplaceWindow.LoginNeeded.Subtitle="먼저 Marketplace 계정을 OBS Studio와 연결하세요" +MarketplaceWindow.LoginNeeded.Title="시작하려면 로그인하세요" +MarketplaceWindow.OpenSettingsButton.Tooltip="설정 열기" +MarketplaceWindow.Purchased.NoPurchasesSubtitle="Marketplace의 디지털 에셋이 여기에 표시됩니다" +MarketplaceWindow.Purchased.NoPurchasesTitle="아직 장면 콜렉션 제품을 보유하고 있지 않습니다" +MarketplaceWindow.Purchased.OpenMarketplaceButton="Marketplace 둘러보기" +MarketplaceWindow.PurchasedTab="라이브러리" +MarketplaceWindow.Settings.Advanced="고급" +MarketplaceWindow.Settings.DefaultAudioDevice.Label="마이크" +MarketplaceWindow.Settings.DefaultAV.Label="기본 입력 장치" +MarketplaceWindow.Settings.DefaultVideoDevice.Label="카메라" +MarketplaceWindow.Settings.DefaultVideoDevice.SettingsButton.Tooltip="비디오 캡처 장치 설정" +MarketplaceWindow.Settings.EnableMakerTools.Tip="수동으로 장면 콜렉션 내보내기 및 불러오기" +MarketplaceWindow.Settings.EnableMakerTools.Tooltip="Elgato Marketplace용 장면 콜렉션을 제작하려는 크리에이터를 위한 내보내기 및 불러오기 도구를 활성화합니다." +MarketplaceWindow.Settings.EnableMakerTools="메이커 도구 활성화" +MarketplaceWindow.Settings.InstallLocation="미디어 파일 폴더" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Disabled="OBS Studio를 다시 시작하면 내보내기 및 불러오기를 더 이상 사용할 수 없습니다" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Enabled="OBS Studio를 다시 시작하면 내보내기 및 불러오기를 사용할 수 있습니다" +MarketplaceWindow.Settings.Title="설정" +MarketplaceWindow.StoreButton.Tooltip="Elgato Marketplace로 이동" +SceneCollectionInfo.CloseButton="닫기" +SceneCollectionInfo.Text="필요한 에셋을 다운로드 및 설치하고, OBS Studio를 다시 시작하면 이 콜렉션을 사용할 수 있습니다" +SceneCollectionInfo.Title="이 장면 콜렉션은 서드파티 에셋이 필요합니다" +SceneCollectionInfo.WindowTitle="장면 콜렉션 정보" +SetupWizard.AudioSetup.Device.Text="마이크를 선택하세요" +SetupWizard.BackButton="뒤로" +SetupWizard.CreateCollection.NewNameLabel="이름" +SetupWizard.CreateCollection.SubTitle="이 이름은 장면 콜렉션 목록에 표시됩니다" +SetupWizard.CreateCollection.Title="장면 콜렉션 이름 지정" +SetupWizard.ImportTitlePrefix="불러오기" +SetupWizard.IncompatibleFile.Text="오류: 이 다운로드에 유효한 bundleInfo.json 파일이 포함되어 있지 않으므로 설치를 진행할 수 없습니다. (서버로 전송된 장면 콜렉션 파일과 관련된 문제입니다)" +SetupWizard.IncompatibleFile.Title="호환되지 않는 파일" +SetupWizard.InstallButton="불러오기" +SetupWizard.Loading.Text="대용량 파일이 포함된 장면 콜렉션의 경우 다소 시간이 걸릴 수 있습니다" +SetupWizard.Loading.Title="콜렉션 로딩 중…" +SetupWizard.MergeCollection="기존 콜렉션과 병합" +SetupWizard.MergeCollectionName.SubTitle="새로 병합된 장면 콜렉션의 이름을 붙여주세요" +SetupWizard.MergeCollectionName.Title="병합된 장면 콜렉션 이름 지정" +SetupWizard.MergeCollectionSteps.ChooseMicrophone="마이크 선택" +SetupWizard.MergeCollectionSteps.GetStarted="시작하기" +SetupWizard.MergeCollectionSteps.SelectScenes="장면 선택" +SetupWizard.MergeCollectionSteps.SetUpCameras="카메라 설정" +SetupWizard.MergeSelectScenes.AllScenes.Tooltip="이 옵션을 선택하면, 장면 콜렉션에 포함된 모든 장면이 현재 콜렉션에 병합됩니다" +SetupWizard.MergeSelectScenes.AllScenes="모든 장면 불러오기" +SetupWizard.MergeSelectScenes.NoCustomMergeAvailable="이 장면 콜렉션은 모든 장면을 한 번에 병합하는 방식만 지원합니다" +SetupWizard.MergeSelectScenes.SubTitle="선택한 모든 장면(및 관련 종속 항목)이 현재 장면 콜렉션에 추가됩니다" +SetupWizard.MergeSelectScenes.Title="병합할 장면을 선택하세요" +SetupWizard.MissingPlugins.DownloadButton="받기" +SetupWizard.MissingPlugins.Text="필요한 플러그인을 다운로드 및 설치한 후, OBS를 재시작하고 다시 시도하세요" +SetupWizard.MissingPlugins.Title.Plural="이 장면 콜렉션에는 {COUNT}개의 플러그인이 필요합니다" +SetupWizard.MissingPlugins.Title.Single="이 장면 콜렉션에는 1개의 플러그인이 필요합니다" +SetupWizard.MissingSourceClone.Description="플러그인을 다운로드 및 설치한 후, OBS Studio를 재시작하고 다시 시도하세요" +SetupWizard.MissingSourceClone.Title="장면 콜렉션을 병합하려면 Source Clone 플러그인이 필요합니다" +SetupWizard.NameInUseError.Text="같은 이름의 장면 콜렉션이 이미 존재합니다. 다른 이름을 입력해주시기 바랍니다." +SetupWizard.NameInUseError.Title="이미 사용 중인 이름" +SetupWizard.NewCollection="신규 생성" +SetupWizard.NewCollectionSteps.ChooseMicrophone="마이크 선택" +SetupWizard.NewCollectionSteps.GetStarted="시작하기" +SetupWizard.NewCollectionSteps.NameSceneCollection="장면 콜렉션 이름 지정" +SetupWizard.NewCollectionSteps.SetUpCameras="카메라 설정" +SetupWizard.NextButton="계속" +SetupWizard.SelectInstall.ExistingInstallButton="추가" +SetupWizard.SelectInstall.NewInstallButton="신규" +SetupWizard.SelectInstall.Title="어떻게 설치하시겠습니까?" +SetupWizard.StartInstallation.Description="먼저, 장면 콜렉션의 이름을 지정하고 입력 장치를 선택하세요" +SetupWizard.VideoSetup.SubTitle.Plural="비디오 소스에 카메라 할당" +SetupWizard.VideoSetup.SubTitle.Singular="비디오 소스에 카메라 할당" +SetupWizard.VideoSetup.Title.Plural="{COUNT}개의 비디오 소스 발견" +SetupWizard.VideoSetup.Title.Singular="1개의 비디오 소스 발견" +SetupWizard.WindowTitle="Elgato Marketplace에서 장면 콜력션 불러오기" +UpdateModal.Description="최신 기능과 향상된 성능을 이용하려면 Elgato Marketplace Connect의 최신 버전을 설치하세요" +UpdateModal.DownloadUpdateButton="지금 업데이트" +UpdateModal.LaterButton="나중에 다시 묻기" +UpdateModal.SkipVersionButton="건너뛰기" +UpdateModal.Title="업데이트 사용 가능" diff --git a/data/locale/nl-NL.ini b/data/locale/nl-NL.ini new file mode 100644 index 0000000..2911d4f --- /dev/null +++ b/data/locale/nl-NL.ini @@ -0,0 +1,129 @@ +AssetProtection.Message="Deze scènecollectie bevat elementen van een aankoop via de Elgato Marketplace. Log in op het account waarmee je deze aankoop hebt gedaan om hem te installeren. Je kunt inloggen via het menu van de Elgato Marketplace en vervolgens opnieuw proberen te importeren." +ExportWizard.BackButton="Terug" +ExportWizard.CloseButton="Gereed" +ExportWizard.Complete="Export van {COLLECTION_NAME} voltooid" +ExportWizard.Export="Exporteren" +ExportWizard.ExportComplete.Text="Je scènecollectie is nu klaar om te testen of om naar Marketplace te uploaden" +ExportWizard.Exporting.Text="Dit kan een bij scènecollecties met grote bestanden een paar minuten duren" +ExportWizard.Exporting.Title="Bundel exporteren…" +ExportWizard.ExportTitle="{COLLECTION_NAME} exporteren" +ExportWizard.GetStartedButton="Set-up" +ExportWizard.MediaFileCheck.SubtitleNone="0 mediabestanden worden opgenomen in de scènecollectie" +ExportWizard.MediaFileCheck.SubtitlePlural="Deze mediabestanden worden opgenomen in de scènecollectie" +ExportWizard.MediaFileCheck.SubtitleSingular="Dit bestand wordt opgenomen in de scènecollectie" +ExportWizard.MediaFileCheck.Title="Je export bekijken" +ExportWizard.MediaFileCheck.TitlePlural="Er zijn {COUNT} mediabestanden gevonden" +ExportWizard.MediaFileCheck.TitleSingular="Er is 1 mediabestand gevonden" +ExportWizard.NextButton="Volgende" +ExportWizard.OutputScenes.Subtitle="Ondertitel uitvoerscènes" +ExportWizard.OutputScenes.Title="Uitvoerscènes selecteren" +ExportWizard.RequiredPlugins.NoPluginsFound="Geen geïnstalleerde plug-ins gevonden" +ExportWizard.RequiredPlugins.PluralTitle="Er zijn {COUNT} plug-ins gevonden" +ExportWizard.RequiredPlugins.SingleTitle="Er is 1 plug-in gevonden" +ExportWizard.RequiredPlugins.SubTitle="Selecteer plug-ins die vereist zijn voor deze scènecollectie" +ExportWizard.StartExport.Description="Laten we eerst de afhankelijkheden verzamelen en de bronnen instellen" +ExportWizard.Steps.BundleRequiredPlugins="Vereiste plug-ins bundelen" +ExportWizard.Steps.CollectMediaFiles="Mediabestanden verzamelen" +ExportWizard.Steps.GetStarted="Aan de slag" +ExportWizard.Steps.RenameVideoSources="Videobronnen hernoemen" +ExportWizard.Steps.SelectOutputScenes="Uitvoerscènes selecteren" +ExportWizard.Steps.ThirdPartyRequirements="Vereisten van derden" +ExportWizard.ThirdPartyRequirements.SubTitle="Geef een lijst op met namen en URL's voor eventuele vereisten van derden." +ExportWizard.ThirdPartyRequirements.Title="Vereisten van derden" +ExportWizard.VideoSourceLabel.CurrentLabel="Huidig" +ExportWizard.VideoSourceLabel.NewLabel="Nieuw" +ExportWizard.VideoSourceLabel.SubTitle="Stel in hoe videobronnen voor gebruikers verschijnen in hun lijst met bronnen" +ExportWizard.VideoSourceLabels.InputPlaceholder="Beschrijvingstekst hier invoegen" +ExportWizard.VideoSourceLabels.NoCaptureSourcesSub="Er worden geen videobronnen opgenomen in de scènecollectie" +ExportWizard.VideoSourceLabels.NoCaptureSourcesText="Er zijn geen videobronnen gevonden" +ExportWizard.VideoSourceLabels.Title="Videobronnen hernoemen" +General.CancelButton="Annuleren" +General.SaveButton="Opslaan" +MarketplaceWindow.ConnectionError.Subtitle="Kon geen verbinding maken met de server. (Controleer je netwerkverbinding)" +MarketplaceWindow.ConnectionError.Title="Verbindingsfout" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Subtitle="Bestand is geen .elgatoscene-bestand" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Title="Ongeldig bestandstype" +MarketplaceWindow.DownloadProduct.NetworkError.Subtitle="Kon geen verbinding maken met de Marketplace. Probeer het opnieuw." +MarketplaceWindow.DownloadProduct.NetworkError.Title="Netwerkverbindingsfout" +MarketplaceWindow.Loading.Title="Je gekochte producten laden…" +MarketplaceWindow.LoggingIn.Subtitle="Werkt het niet? Klik op 'Opnieuw proberen' om opnieuw te openen in de browser" +MarketplaceWindow.LoggingIn.Title="Naar de browser gaan om in te loggen" +MarketplaceWindow.LoggingIn.TryAgain="Opnieuw proberen" +MarketplaceWindow.LoginButton.LogIn="Inloggen" +MarketplaceWindow.LoginError.Subtitle="Er was een probleem bij het inloggen. Probeer opnieuw in te loggen om door te gaan." +MarketplaceWindow.LoginError.Title="Inloggen" +MarketplaceWindow.LoginNeeded.Subtitle="Laten we eerst je Marketplace-account met OBS Studio verbinden" +MarketplaceWindow.LoginNeeded.Title="Log in om aan de slag te gaan" +MarketplaceWindow.OpenSettingsButton.Tooltip="Instellingen openen" +MarketplaceWindow.Purchased.NoPurchasesSubtitle="Je digitale assets van Marketplace verschijnen hier" +MarketplaceWindow.Purchased.NoPurchasesTitle="Je hebt nog geen scènecollectie-producten" +MarketplaceWindow.Purchased.OpenMarketplaceButton="Marketplace verkennen" +MarketplaceWindow.PurchasedTab="Je bibliotheek" +MarketplaceWindow.Settings.Advanced="Geavanceerd" +MarketplaceWindow.Settings.DefaultAudioDevice.Label="Microfoon" +MarketplaceWindow.Settings.DefaultAV.Label="Standaard invoerapparaten" +MarketplaceWindow.Settings.DefaultVideoDevice.Label="Camera" +MarketplaceWindow.Settings.DefaultVideoDevice.SettingsButton.Tooltip="Instellingen van video-opnameapparaat" +MarketplaceWindow.Settings.EnableMakerTools.Tip="Scènecollecties handmatig exporteren en importeren" +MarketplaceWindow.Settings.EnableMakerTools.Tooltip="Dit schakelt export- en importtools in voor makers die scènecollecties willen maken voor de Elgato Marketplace." +MarketplaceWindow.Settings.EnableMakerTools="Tools voor makers inschakelen" +MarketplaceWindow.Settings.InstallLocation="Map met mediabestanden" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Disabled="Exporteren en importeren is niet langer beschikbaar na het herstarten van OBS Studio" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Enabled="Exporteren en importeren zal beschikbaar zijn na het herstarten van OBS Studio" +MarketplaceWindow.Settings.Title="Instellingen" +MarketplaceWindow.StoreButton.Tooltip="Ga naar Elgato Marketplace" +SceneCollectionInfo.CloseButton="Sluiten" +SceneCollectionInfo.Text="Download en installeer de vereiste assets en herstart OBS Studio vervolgens om deze collectie te gebruiken" +SceneCollectionInfo.Title="Assets van derden zijn vereist voor deze scènecollectie" +SceneCollectionInfo.WindowTitle="Scènecollectie-informatie" +SetupWizard.AudioSetup.Device.Text="Selecteer een microfoon" +SetupWizard.BackButton="Terug" +SetupWizard.CreateCollection.NewNameLabel="Naam" +SetupWizard.CreateCollection.SubTitle="Deze naam verschijnt in je lijst met scènecollecties" +SetupWizard.CreateCollection.Title="Naam scènecollectie" +SetupWizard.ImportTitlePrefix="Importeren" +SetupWizard.IncompatibleFile.Text="Fout: deze download bevatte geen geldig bundleInfo.json-bestand en kan niet worden geïnstalleerd. (Dit is een probleem met het ingediende scènecollectie-bestand op de server)" +SetupWizard.IncompatibleFile.Title="Incompatibel bestand" +SetupWizard.InstallButton="Importeren" +SetupWizard.Loading.Text="Dit kan een bij scènecollecties met grote bestanden even duren" +SetupWizard.Loading.Title="Collectie laden…" +SetupWizard.MergeCollection="Met huidige samenvoegen" +SetupWizard.MergeCollectionName.SubTitle="Geef een naam aan de nieuwe samengevoegde scènecollectie" +SetupWizard.MergeCollectionName.Title="Naam geven aan samengevoegde scènecollectie" +SetupWizard.MergeCollectionSteps.ChooseMicrophone="Microfoon kiezen" +SetupWizard.MergeCollectionSteps.GetStarted="Aan de slag" +SetupWizard.MergeCollectionSteps.SelectScenes="Scènes selecteren" +SetupWizard.MergeCollectionSteps.SetUpCameras="Camera's instellen" +SetupWizard.MergeSelectScenes.AllScenes.Tooltip="Als deze optie is aangevinkt, worden alle scènes uit de scènecollectie samengevoegd in de huidige verzameling." +SetupWizard.MergeSelectScenes.AllScenes="Alle scènes importeren" +SetupWizard.MergeSelectScenes.NoCustomMergeAvailable="Voor deze scènecollectie kunnen alleen alle scènes tegelijk worden samengevoegd" +SetupWizard.MergeSelectScenes.SubTitle="Alle geselecteerde scènes (en afhankelijkheden) worden toegevoegd aan de huidige scènecollectie" +SetupWizard.MergeSelectScenes.Title="Selecteer scènes om samen te voegen" +SetupWizard.MissingPlugins.DownloadButton="Downloaden" +SetupWizard.MissingPlugins.Text="Download en installeer de vereiste plug-ins, herstart OBS en probeer het opnieuw" +SetupWizard.MissingPlugins.Title.Plural="Er zijn {COUNT} plug-ins vereist voor deze scènecollectie" +SetupWizard.MissingPlugins.Title.Single="Er is 1 plug-in vereist voor deze scènecollectie" +SetupWizard.MissingSourceClone.Description="Download en installeer de plug-in, herstart OBS Studio en probeer het opnieuw" +SetupWizard.MissingSourceClone.Title="De plug-in Source Clone is vereist om scènecollecties samen te voegen" +SetupWizard.NameInUseError.Text="Er bestaat al een scènecollectie met deze naam. Geef een andere naam op." +SetupWizard.NameInUseError.Title="Naam in gebruik" +SetupWizard.NewCollection="Nieuwe maken" +SetupWizard.NewCollectionSteps.ChooseMicrophone="Microfoon kiezen" +SetupWizard.NewCollectionSteps.GetStarted="Aan de slag" +SetupWizard.NewCollectionSteps.NameSceneCollection="Naam scènecollectie" +SetupWizard.NewCollectionSteps.SetUpCameras="Camera's instellen" +SetupWizard.NextButton="Doorgaan" +SetupWizard.SelectInstall.ExistingInstallButton="Toevoegen" +SetupWizard.SelectInstall.NewInstallButton="Nieuw" +SetupWizard.SelectInstall.Title="Hoe wil je dit installeren?" +SetupWizard.StartInstallation.Description="Laten we de scènecollectie eerst een naam geven en je invoerapparaten selecteren" +SetupWizard.VideoSetup.SubTitle.Plural="Camera's aan videobronnen toewijzen" +SetupWizard.VideoSetup.SubTitle.Singular="Wijs een camera aan de videobron toe" +SetupWizard.VideoSetup.Title.Plural="{COUNT} videobronnen gevonden" +SetupWizard.VideoSetup.Title.Singular="1 videobron gevonden" +SetupWizard.WindowTitle="Scènecollectie van Elgato Marketplace importeren" +UpdateModal.Description="Download de laatste versie van Elgato Marketplace Connect voor toegang tot nieuwe functies en verbeteringen" +UpdateModal.DownloadUpdateButton="Nu bijwerken" +UpdateModal.LaterButton="Vraag me later" +UpdateModal.SkipVersionButton="Nee, bedankt" +UpdateModal.Title="Update beschikbaar" diff --git a/data/locale/pt-BR.ini b/data/locale/pt-BR.ini new file mode 100644 index 0000000..3983aeb --- /dev/null +++ b/data/locale/pt-BR.ini @@ -0,0 +1,129 @@ +AssetProtection.Message="Esta cena contém elementos de uma compra feita no Elgato Marketplace. Para instalá-la, use o menu Elgato Marketplace para iniciar a sessão com a conta usada para fazer a compra e tente importar novamente." +ExportWizard.BackButton="Voltar" +ExportWizard.CloseButton="OK" +ExportWizard.Complete="Exportação de {COLLECTION_NAME} concluída" +ExportWizard.Export="Exportar" +ExportWizard.ExportComplete.Text="A coleção de cenas está pronta para ser testada ou carregada no Marketplace" +ExportWizard.Exporting.Text="O processo pode levar alguns minutos no caso de coleções de cenas com arquivos grandes" +ExportWizard.Exporting.Title="Exportando pacote…" +ExportWizard.ExportTitle="Exportação de {COLLECTION_NAME}" +ExportWizard.GetStartedButton="Configurar" +ExportWizard.MediaFileCheck.SubtitleNone="Nenhum arquivo de mídia será incluído com a coleção de cenas" +ExportWizard.MediaFileCheck.SubtitlePlural="Estes arquivos serão incluídos com a coleção de cenas" +ExportWizard.MediaFileCheck.SubtitleSingular="Este arquivo será incluído com a coleção de cenas" +ExportWizard.MediaFileCheck.Title="Revise a exportação" +ExportWizard.MediaFileCheck.TitlePlural="{COUNT} arquivos de mídia foram encontrados" +ExportWizard.MediaFileCheck.TitleSingular="1 arquivo de mídia foi encontrado" +ExportWizard.NextButton="Seguinte" +ExportWizard.OutputScenes.Subtitle="Legenda das cenas de saída" +ExportWizard.OutputScenes.Title="Selecione as cenas de saída" +ExportWizard.RequiredPlugins.NoPluginsFound="Nenhum plug-in instalado foi encontrado" +ExportWizard.RequiredPlugins.PluralTitle="{COUNT} plug‑ins foram encontrados" +ExportWizard.RequiredPlugins.SingleTitle="1 plug‑in foi encontrado" +ExportWizard.RequiredPlugins.SubTitle="Selecione os plug‑ins necessários para esta coleção de cenas" +ExportWizard.StartExport.Description="Primeiro, reúna as dependências e configure as fontes" +ExportWizard.Steps.BundleRequiredPlugins="Reúna os plug‑ins necessários" +ExportWizard.Steps.CollectMediaFiles="Reúna os arquivos de mídia" +ExportWizard.Steps.GetStarted="Começar" +ExportWizard.Steps.RenameVideoSources="Renomeie as fontes de vídeo" +ExportWizard.Steps.SelectOutputScenes="Selecione as cenas de saída" +ExportWizard.Steps.ThirdPartyRequirements="Requisitos de terceiros" +ExportWizard.ThirdPartyRequirements.SubTitle="Forneça uma lista de nomes e URLs de qualquer requisito de terceiros." +ExportWizard.ThirdPartyRequirements.Title="Requisitos de terceiros" +ExportWizard.VideoSourceLabel.CurrentLabel="Atual" +ExportWizard.VideoSourceLabel.NewLabel="Nova" +ExportWizard.VideoSourceLabel.SubTitle="Defina como as fontes de vídeo aparecem na lista de fontes de outras pessoas" +ExportWizard.VideoSourceLabels.InputPlaceholder="Texto descritivo" +ExportWizard.VideoSourceLabels.NoCaptureSourcesSub="Nenhuma fonte de vídeo será incluída com a coleção de cenas" +ExportWizard.VideoSourceLabels.NoCaptureSourcesText="Nenhuma fonte de vídeo foi encontrada" +ExportWizard.VideoSourceLabels.Title="Renomeie as fontes de vídeo" +General.CancelButton="Cancelar" +General.SaveButton="Salvar" +MarketplaceWindow.ConnectionError.Subtitle="Não foi possível se conectar ao servidor (verifique a conexão de rede)" +MarketplaceWindow.ConnectionError.Title="Erro de conexão" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Subtitle="A extensão do arquivo não é .elgatoscene" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Title="Tipo de arquivo inválido" +MarketplaceWindow.DownloadProduct.NetworkError.Subtitle="Não foi possível se conectar ao Marketplace. Tente novamente." +MarketplaceWindow.DownloadProduct.NetworkError.Title="Erro de conexão de rede" +MarketplaceWindow.Loading.Title="Carregando produtos comprados…" +MarketplaceWindow.LoggingIn.Subtitle="Não está funcionando? Clique em “Tentar novamente” para reabrir no navegador." +MarketplaceWindow.LoggingIn.Title="Comunicando‑se com o navegador para iniciar a sessão" +MarketplaceWindow.LoggingIn.TryAgain="Tentar novamente" +MarketplaceWindow.LoginButton.LogIn="Iniciar sessão" +MarketplaceWindow.LoginError.Subtitle="Houve um problema ao iniciar a sessão. Tente novamente." +MarketplaceWindow.LoginError.Title="Inicie a sessão" +MarketplaceWindow.LoginNeeded.Subtitle="Primeiro, conecte sua conta do Marketplace ao OBS Studio" +MarketplaceWindow.LoginNeeded.Title="Inicie a sessão para começar" +MarketplaceWindow.OpenSettingsButton.Tooltip="Abrir configurações" +MarketplaceWindow.Purchased.NoPurchasesSubtitle="Seus materiais digitais do Marketplace aparecerão aqui" +MarketplaceWindow.Purchased.NoPurchasesTitle="Você ainda não tem nenhum produto de coleção de cenas" +MarketplaceWindow.Purchased.OpenMarketplaceButton="Explorar Marketplace" +MarketplaceWindow.PurchasedTab="Sua biblioteca" +MarketplaceWindow.Settings.Advanced="Avançado" +MarketplaceWindow.Settings.DefaultAudioDevice.Label="Microfone" +MarketplaceWindow.Settings.DefaultAV.Label="Dispositivos de entrada padrão" +MarketplaceWindow.Settings.DefaultVideoDevice.Label="Câmera" +MarketplaceWindow.Settings.DefaultVideoDevice.SettingsButton.Tooltip="Configurações do dispositivo de captura de vídeo" +MarketplaceWindow.Settings.EnableMakerTools.Tip="Exporte e importe manualmente as coleções de cenas" +MarketplaceWindow.Settings.EnableMakerTools.Tooltip="Habilita as ferramentas de exportação e importação para quem quiser criar coleções de cenas para o Elgato Marketplace." +MarketplaceWindow.Settings.EnableMakerTools="Habilitar ferramentas de criação" +MarketplaceWindow.Settings.InstallLocation="Pasta dos arquivos de mídia" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Disabled="A exportação e importação não estarão mais disponíveis depois de reiniciar o OBS Studio" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Enabled="A exportação e importação estarão disponíveis depois de reiniciar o OBS Studio" +MarketplaceWindow.Settings.Title="Configurações" +MarketplaceWindow.StoreButton.Tooltip="Abre o Elgato Marketplace" +SceneCollectionInfo.CloseButton="Fechar" +SceneCollectionInfo.Text="Para usar esta coleção, baixe e instale os materiais necessários e reinicie o OBS Studio" +SceneCollectionInfo.Title="Esta coleção de cenas requer materiais de terceiros" +SceneCollectionInfo.WindowTitle="Informações da coleção de cenas" +SetupWizard.AudioSetup.Device.Text="Selecione um microfone" +SetupWizard.BackButton="Voltar" +SetupWizard.CreateCollection.NewNameLabel="Nome" +SetupWizard.CreateCollection.SubTitle="Este nome aparecerá na sua lista de coleções de cenas" +SetupWizard.CreateCollection.Title="Nomeie a coleção de cenas" +SetupWizard.ImportTitlePrefix="Importar" +SetupWizard.IncompatibleFile.Text="Erro: não é possível instalar este download porque ele não contém um arquivo bundleInfo.json válido (o problema está no arquivo de coleção de cenas armazenado no servidor)." +SetupWizard.IncompatibleFile.Title="Arquivo incompatível" +SetupWizard.InstallButton="Importar" +SetupWizard.Loading.Text="O processo pode levar um tempo no caso de coleções de cenas com arquivos grandes" +SetupWizard.Loading.Title="Carregando coleção…" +SetupWizard.MergeCollection="Combinar com atual" +SetupWizard.MergeCollectionName.SubTitle="Nomeie a nova coleção de cenas combinadas" +SetupWizard.MergeCollectionName.Title="Nomeie a coleção de cenas combinadas" +SetupWizard.MergeCollectionSteps.ChooseMicrophone="Escolha o microfone" +SetupWizard.MergeCollectionSteps.GetStarted="Início" +SetupWizard.MergeCollectionSteps.SelectScenes="Selecione as cenas" +SetupWizard.MergeCollectionSteps.SetUpCameras="Configure as câmeras" +SetupWizard.MergeSelectScenes.AllScenes.Tooltip="Com esta opção selecionada, todas as cenas fornecidas pela coleção de cenas serão combinadas com a coleção atual" +SetupWizard.MergeSelectScenes.AllScenes="Importar todas as cenas" +SetupWizard.MergeSelectScenes.NoCustomMergeAvailable="Esta coleção de cenas só permite combinar todas as cenas de uma vez" +SetupWizard.MergeSelectScenes.SubTitle="Todas as cenas combinadas (e suas dependências) serão adicionadas à coleção de cenas atual" +SetupWizard.MergeSelectScenes.Title="Selecione as cenas a combinar" +SetupWizard.MissingPlugins.DownloadButton="Obter" +SetupWizard.MissingPlugins.Text="Baixe e instale os plug‑ins necessários, reinicie o OBS Studio e tente novamente" +SetupWizard.MissingPlugins.Title.Plural="{COUNT} plug‑ins são necessários para esta coleção de cenas" +SetupWizard.MissingPlugins.Title.Single="1 plug‑in é necessário para esta coleção de cenas" +SetupWizard.MissingSourceClone.Description="Baixe e instale o plug‑in, reinicie o OBS Studio e tente novamente" +SetupWizard.MissingSourceClone.Title="O plug‑in Source Clone é necessário para combinar coleções de cenas" +SetupWizard.NameInUseError.Text="Já existe uma coleção de cenas com este nome. Forneça outro nome." +SetupWizard.NameInUseError.Title="Nome em uso" +SetupWizard.NewCollection="Criar nova" +SetupWizard.NewCollectionSteps.ChooseMicrophone="Escolha o microfone" +SetupWizard.NewCollectionSteps.GetStarted="Início" +SetupWizard.NewCollectionSteps.NameSceneCollection="Nomeie a coleção de cenas" +SetupWizard.NewCollectionSteps.SetUpCameras="Configure as câmeras" +SetupWizard.NextButton="Continuar" +SetupWizard.SelectInstall.ExistingInstallButton="Adicionar" +SetupWizard.SelectInstall.NewInstallButton="Nova" +SetupWizard.SelectInstall.Title="Que tipo de instalação você quer fazer?" +SetupWizard.StartInstallation.Description="Primeiro, nomeie a coleção de cenas e selecione os dispositivos de entrada" +SetupWizard.VideoSetup.SubTitle.Plural="Atribua câmeras às fontes de vídeo" +SetupWizard.VideoSetup.SubTitle.Singular="Atribua uma câmera à fonte de vídeo" +SetupWizard.VideoSetup.Title.Plural="{COUNT} fontes de vídeo foram encontradas" +SetupWizard.VideoSetup.Title.Singular="1 fonte de vídeo foi encontrada" +SetupWizard.WindowTitle="Importe a coleção de cenas do Elgato Marketplace" +UpdateModal.Description="Para acessar novos recursos e melhorias, obtenha a versão mais recente do Elgato Marketplace Connect" +UpdateModal.DownloadUpdateButton="Atualizar agora" +UpdateModal.LaterButton="Perguntar depois" +UpdateModal.SkipVersionButton="Não" +UpdateModal.Title="Atualização disponível" diff --git a/data/locale/pt-PT.ini b/data/locale/pt-PT.ini new file mode 100644 index 0000000..d4d830a --- /dev/null +++ b/data/locale/pt-PT.ini @@ -0,0 +1,129 @@ +AssetProtection.Message="Esta coleção de cenários contém elementos de uma compra feita no Elgato Marketplace. Para a instalar, use o menu Elgato Marketplace para iniciar sessão com a conta usada para fazer a compra e tente importar novamente." +ExportWizard.BackButton="Voltar" +ExportWizard.CloseButton="OK" +ExportWizard.Complete="Exportação de {COLLECTION_NAME} concluída" +ExportWizard.Export="Exportar" +ExportWizard.ExportComplete.Text="A coleção de cenários está pronta para ser testada ou carregada no Marketplace" +ExportWizard.Exporting.Text="O processo pode demorar alguns minutos no caso de coleções de cenários com ficheiros grandes" +ExportWizard.Exporting.Title="A exportar pacote…" +ExportWizard.ExportTitle="Exportar {COLLECTION_NAME}" +ExportWizard.GetStartedButton="Configurar" +ExportWizard.MediaFileCheck.SubtitleNone="Não será incluído nenhum ficheiro multimédia com a coleção de cenários" +ExportWizard.MediaFileCheck.SubtitlePlural="Estes ficheiros serão incluídos com a coleção de cenários" +ExportWizard.MediaFileCheck.SubtitleSingular="Este ficheiro será incluído com a coleção de cenários" +ExportWizard.MediaFileCheck.Title="Reveja a exportação" +ExportWizard.MediaFileCheck.TitlePlural="Foram encontrados {COUNT} ficheiros multimédia" +ExportWizard.MediaFileCheck.TitleSingular="Foi encontrado 1 ficheiro multimédia" +ExportWizard.NextButton="Seguinte" +ExportWizard.OutputScenes.Subtitle="Legenda dos cenários de saída" +ExportWizard.OutputScenes.Title="Selecionar cenários de saída" +ExportWizard.RequiredPlugins.NoPluginsFound="Não foi encontrado nenhum plug-in instalado" +ExportWizard.RequiredPlugins.PluralTitle="Foram encontrados {COUNT} plug-ins" +ExportWizard.RequiredPlugins.SingleTitle="Foi encontrado 1 plug-in" +ExportWizard.RequiredPlugins.SubTitle="Selecione os plug‑ins necessários para esta coleção de cenários" +ExportWizard.StartExport.Description="Primeiro, reúna as dependências e configure as fontes" +ExportWizard.Steps.BundleRequiredPlugins="Reunir plug‑ins necessários" +ExportWizard.Steps.CollectMediaFiles="Reunir ficheiros multimédia" +ExportWizard.Steps.GetStarted="Começar" +ExportWizard.Steps.RenameVideoSources="Alterar nome das fontes de vídeo" +ExportWizard.Steps.SelectOutputScenes="Selecionar cenários de saída" +ExportWizard.Steps.ThirdPartyRequirements="Requisitos de terceiros" +ExportWizard.ThirdPartyRequirements.SubTitle="Forneça uma lista de nomes e URL de qualquer requisito de terceiros." +ExportWizard.ThirdPartyRequirements.Title="Requisitos de terceiros" +ExportWizard.VideoSourceLabel.CurrentLabel="Atual" +ExportWizard.VideoSourceLabel.NewLabel="Nova" +ExportWizard.VideoSourceLabel.SubTitle="Defina como as fontes de vídeo aparecem na lista de fontes de outras pessoas" +ExportWizard.VideoSourceLabels.InputPlaceholder="O texto descritivo fica aqui" +ExportWizard.VideoSourceLabels.NoCaptureSourcesSub="Não será incluído nenhuma fonte de vídeo com a coleção de cenários" +ExportWizard.VideoSourceLabels.NoCaptureSourcesText="Não foi encontrada nenhuma fonte de vídeo" +ExportWizard.VideoSourceLabels.Title="Alterar nome das fontes de vídeo" +General.CancelButton="Cancelar" +General.SaveButton="Guardar" +MarketplaceWindow.ConnectionError.Subtitle="Não foi possível estabelecer ligação ao servidor (verifique a ligação de rede)" +MarketplaceWindow.ConnectionError.Title="Erro de ligação" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Subtitle="A extensão do ficheiro não é .elgatoscene" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Title="Tipo de ficheiro inválido" +MarketplaceWindow.DownloadProduct.NetworkError.Subtitle="Não foi possível estabelecer ligação ao Marketplace. Tente novamente." +MarketplaceWindow.DownloadProduct.NetworkError.Title="Erro de ligação de rede" +MarketplaceWindow.Loading.Title="A carregar produtos comprados…" +MarketplaceWindow.LoggingIn.Subtitle="Não está a funcionar? Clique em “Tentar novamente” para reabrir no navegador." +MarketplaceWindow.LoggingIn.Title="A reencaminhar para o navegador para iniciar sessão" +MarketplaceWindow.LoggingIn.TryAgain="Tentar novamente" +MarketplaceWindow.LoginButton.LogIn="Iniciar sessão" +MarketplaceWindow.LoginError.Subtitle="Ocorreu um problema ao iniciar sessão. Tente novamente." +MarketplaceWindow.LoginError.Title="Iniciar sessão" +MarketplaceWindow.LoginNeeded.Subtitle="Primeiro, ligue a sua conta do Marketplace ao OBS Studio" +MarketplaceWindow.LoginNeeded.Title="Inicie sessão para começar" +MarketplaceWindow.OpenSettingsButton.Tooltip="Abrir definições" +MarketplaceWindow.Purchased.NoPurchasesSubtitle="Os seus materiais digitais do Marketplace aparecerão aqui" +MarketplaceWindow.Purchased.NoPurchasesTitle="Ainda não tem nenhum produto de coleção de cenários" +MarketplaceWindow.Purchased.OpenMarketplaceButton="Explorar o Marketplace" +MarketplaceWindow.PurchasedTab="A sua biblioteca" +MarketplaceWindow.Settings.Advanced="Avançadas" +MarketplaceWindow.Settings.DefaultAudioDevice.Label="Microfone" +MarketplaceWindow.Settings.DefaultAV.Label="Dispositivos de entrada padrão" +MarketplaceWindow.Settings.DefaultVideoDevice.Label="Câmara" +MarketplaceWindow.Settings.DefaultVideoDevice.SettingsButton.Tooltip="Definições do dispositivo de captura de vídeo" +MarketplaceWindow.Settings.EnableMakerTools.Tip="Exporte e importe manualmente as coleções de cenários" +MarketplaceWindow.Settings.EnableMakerTools.Tooltip="Ativa as ferramentas de exportação e importação para quem quiser criar coleções de cenários para o Elgato Marketplace." +MarketplaceWindow.Settings.EnableMakerTools="Ativar ferramentas de criação" +MarketplaceWindow.Settings.InstallLocation="Pasta dos ficheiros multimédia" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Disabled="A exportação e importação deixarão de estar disponíveis depois de reiniciar o OBS Studio" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Enabled="A exportação e importação ficarão disponíveis depois de reiniciar o OBS Studio" +MarketplaceWindow.Settings.Title="Definições" +MarketplaceWindow.StoreButton.Tooltip="Ir para o Elgato Marketplace" +SceneCollectionInfo.CloseButton="Fechar" +SceneCollectionInfo.Text="Para usar esta coleção, descarregue e instale os materiais necessários e reinicie o OBS Studio" +SceneCollectionInfo.Title="Esta coleção de cenários requer materiais de terceiros" +SceneCollectionInfo.WindowTitle="Informações da coleção de cenários" +SetupWizard.AudioSetup.Device.Text="Selecione um microfone" +SetupWizard.BackButton="Voltar" +SetupWizard.CreateCollection.NewNameLabel="Nome" +SetupWizard.CreateCollection.SubTitle="Este nome aparecerá na sua lista de coleções de cenários" +SetupWizard.CreateCollection.Title="Dar nome à coleção de cenários" +SetupWizard.ImportTitlePrefix="Importar" +SetupWizard.IncompatibleFile.Text="Erro: não é possível instalar esta descarga porque não contém um ficheiro bundleInfo.json válido (o problema está no ficheiro de coleção de cenários armazenado no servidor)." +SetupWizard.IncompatibleFile.Title="Ficheiro incompatível" +SetupWizard.InstallButton="Importar" +SetupWizard.Loading.Text="O processo pode demorar algum tempo no caso de coleções de cenários com ficheiros grandes" +SetupWizard.Loading.Title="A carregar coleção…" +SetupWizard.MergeCollection="Combinar com atual" +SetupWizard.MergeCollectionName.SubTitle="Dê um nome à nova coleção de cenários combinados" +SetupWizard.MergeCollectionName.Title="Dar nome à coleção de cenários combinados" +SetupWizard.MergeCollectionSteps.ChooseMicrophone="Escolher o microfone" +SetupWizard.MergeCollectionSteps.GetStarted="Começar" +SetupWizard.MergeCollectionSteps.SelectScenes="Selecionar os cenários" +SetupWizard.MergeCollectionSteps.SetUpCameras="Configurar as câmaras" +SetupWizard.MergeSelectScenes.AllScenes.Tooltip="Com esta opção selecionada, todos os cenários fornecidos pela coleção de cenários serão combinados com a coleção atual" +SetupWizard.MergeSelectScenes.AllScenes="Importar todos os cenários" +SetupWizard.MergeSelectScenes.NoCustomMergeAvailable="Esta coleção de cenários só permite combinar todos os cenários de uma vez" +SetupWizard.MergeSelectScenes.SubTitle="Todos os cenários combinados (e respetivas dependências) serão adicionados à coleção de cenários atual" +SetupWizard.MergeSelectScenes.Title="Selecione os cenários a combinar" +SetupWizard.MissingPlugins.DownloadButton="Obter" +SetupWizard.MissingPlugins.Text="Descarregue e instale os plug‑ins necessários, reinicie o OBS Studio e tente novamente" +SetupWizard.MissingPlugins.Title.Plural="São necessários {COUNT} plug‑ins para esta coleção de cenários" +SetupWizard.MissingPlugins.Title.Single="É necessário 1 plug‑in para esta coleção de cenários" +SetupWizard.MissingSourceClone.Description="Descarregue e instale os plug‑ins, reinicie o OBS Studio e tente novamente" +SetupWizard.MissingSourceClone.Title="O plug‑in Source Clone é necessário para combinar coleções de cenários" +SetupWizard.NameInUseError.Text="Já existe uma coleção de cenários com este nome. Forneça outro nome." +SetupWizard.NameInUseError.Title="Nome em utilização" +SetupWizard.NewCollection="Criar nova" +SetupWizard.NewCollectionSteps.ChooseMicrophone="Escolher o microfone" +SetupWizard.NewCollectionSteps.GetStarted="Começar" +SetupWizard.NewCollectionSteps.NameSceneCollection="Dar nome à coleção de cenários" +SetupWizard.NewCollectionSteps.SetUpCameras="Configurar as câmaras" +SetupWizard.NextButton="Continuar" +SetupWizard.SelectInstall.ExistingInstallButton="Adicionar" +SetupWizard.SelectInstall.NewInstallButton="Nova" +SetupWizard.SelectInstall.Title="Como quer instalar isto?" +SetupWizard.StartInstallation.Description="Primeiro, dê um à coleção de cenários e selecione os dispositivos de entrada" +SetupWizard.VideoSetup.SubTitle.Plural="Atribua câmaras às fontes de vídeo" +SetupWizard.VideoSetup.SubTitle.Singular="Atribua uma câmara à fonte de vídeo" +SetupWizard.VideoSetup.Title.Plural="Foram encontradas {COUNT} fontes de vídeo" +SetupWizard.VideoSetup.Title.Singular="Foi encontrada 1 fonte de vídeo" +SetupWizard.WindowTitle="Importe a coleção de cenários do Elgato Marketplace" +UpdateModal.Description="Para aceder a novas funcionalidades e melhorias, obtenha a versão mais recente do Elgato Marketplace Connect" +UpdateModal.DownloadUpdateButton="Atualizar agora" +UpdateModal.LaterButton="Perguntar depois" +UpdateModal.SkipVersionButton="Não, obrigado" +UpdateModal.Title="Atualização disponível" diff --git a/data/locale/ru-RU.ini b/data/locale/ru-RU.ini new file mode 100644 index 0000000..3c28d74 --- /dev/null +++ b/data/locale/ru-RU.ini @@ -0,0 +1,129 @@ +AssetProtection.Message="Эта коллекция сцен содержит элементы, купленные в Elgato Marketplace. Чтобы установить ее, войдите в систему с учетной записью пользователя, сделавшего покупку. Сделать это можно через меню Elgato Marketplace. Затем импортируйте коллекцию еще раз." +ExportWizard.BackButton="Назад" +ExportWizard.CloseButton="Готово" +ExportWizard.Complete="Коллекция {COLLECTION_NAME} экспортирована" +ExportWizard.Export="Экспорт" +ExportWizard.ExportComplete.Text="Ваша коллекция сцен готова к тестированию и отправке на Marketplace" +ExportWizard.Exporting.Text="Если коллекция содержит большие файлы, экспорт может занять некоторое время" +ExportWizard.Exporting.Title="Идет экспорт пакета…" +ExportWizard.ExportTitle="Экспорт коллекции {COLLECTION_NAME}" +ExportWizard.GetStartedButton="Настроить" +ExportWizard.MediaFileCheck.SubtitleNone="В коллекцию сцен не будут включены медиафайлы" +ExportWizard.MediaFileCheck.SubtitlePlural="Эти файлы будут включены в коллекцию сцен" +ExportWizard.MediaFileCheck.SubtitleSingular="Этот файл будет включен в коллекцию сцен" +ExportWizard.MediaFileCheck.Title="Проверка перед экспортом" +ExportWizard.MediaFileCheck.TitlePlural="Найдено медиафайлов: {COUNT}" +ExportWizard.MediaFileCheck.TitleSingular="Найден 1 медиафайл" +ExportWizard.NextButton="Далее" +ExportWizard.OutputScenes.Subtitle="Подзаголовок выводимых сцен" +ExportWizard.OutputScenes.Title="Выбор выводимых сцен" +ExportWizard.RequiredPlugins.NoPluginsFound="Установленных плагинов не найдено" +ExportWizard.RequiredPlugins.PluralTitle="Найдено плагинов: {COUNT}" +ExportWizard.RequiredPlugins.SingleTitle="Найден 1 плагин" +ExportWizard.RequiredPlugins.SubTitle="Выберите плагины, необходимые для этой коллекции сцен" +ExportWizard.StartExport.Description="Для начала давайте определим зависимые компоненты и настроим источники" +ExportWizard.Steps.BundleRequiredPlugins="Сбор необходимых плагинов" +ExportWizard.Steps.CollectMediaFiles="Сбор медиафайлов" +ExportWizard.Steps.GetStarted="Первые шаги" +ExportWizard.Steps.RenameVideoSources="Переименование источников видео" +ExportWizard.Steps.SelectOutputScenes="Выбор выводимых сцен" +ExportWizard.Steps.ThirdPartyRequirements="Сторонние компоненты" +ExportWizard.ThirdPartyRequirements.SubTitle="Перечислите названия и URL-адреса необходимых сторонних компонентов." +ExportWizard.ThirdPartyRequirements.Title="Сторонние компоненты" +ExportWizard.VideoSourceLabel.CurrentLabel="Сейчас" +ExportWizard.VideoSourceLabel.NewLabel="Новое" +ExportWizard.VideoSourceLabel.SubTitle="Укажите названия, под которыми источники видео будут отображаться в списках источников других пользователей" +ExportWizard.VideoSourceLabels.InputPlaceholder="Текст-описание" +ExportWizard.VideoSourceLabels.NoCaptureSourcesSub="В коллекцию сцен не будут включены источники видео" +ExportWizard.VideoSourceLabels.NoCaptureSourcesText="Источников видео не найдено" +ExportWizard.VideoSourceLabels.Title="Переименование источников видео" +General.CancelButton="Отменить" +General.SaveButton="Сохранить" +MarketplaceWindow.ConnectionError.Subtitle="Не удалось установить соединение с сервером. Проверьте сетевое подключение." +MarketplaceWindow.ConnectionError.Title="Ошибка подключения" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Subtitle="Формат файла — не .elgatoscene" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Title="Недопустимый тип файла" +MarketplaceWindow.DownloadProduct.NetworkError.Subtitle="Не удалось установить соединение с Marketplace. Повторите попытку." +MarketplaceWindow.DownloadProduct.NetworkError.Title="Ошибка подключения к сети" +MarketplaceWindow.Loading.Title="Идет загрузка купленных элементов…" +MarketplaceWindow.LoggingIn.Subtitle="Что-то не получается? Нажмите «Повторить попытку», чтобы заново открыть страницу в браузере" +MarketplaceWindow.LoggingIn.Title="Перенаправляем вас в браузер для входа в систему" +MarketplaceWindow.LoggingIn.TryAgain="Повторить попытку" +MarketplaceWindow.LoginButton.LogIn="Вход" +MarketplaceWindow.LoginError.Subtitle="При входе в систему произошел сбой. Повторите попытку." +MarketplaceWindow.LoginError.Title="Вход" +MarketplaceWindow.LoginNeeded.Subtitle="Для начала давайте свяжем ваш аккаунт Marketplace с OBS Studio" +MarketplaceWindow.LoginNeeded.Title="Войдите в систему" +MarketplaceWindow.OpenSettingsButton.Tooltip="Открыть настройки" +MarketplaceWindow.Purchased.NoPurchasesSubtitle="Ваши цифровые ресурсы из Marketplace появятся здесь" +MarketplaceWindow.Purchased.NoPurchasesTitle="Пока у вас нет никаких объектов, входящих в коллекции сцен" +MarketplaceWindow.Purchased.OpenMarketplaceButton="Открыть Marketplace" +MarketplaceWindow.PurchasedTab="Библиотека" +MarketplaceWindow.Settings.Advanced="Дополнительно" +MarketplaceWindow.Settings.DefaultAudioDevice.Label="Микрофон" +MarketplaceWindow.Settings.DefaultAV.Label="Устройства ввода по умолчанию" +MarketplaceWindow.Settings.DefaultVideoDevice.Label="Камера" +MarketplaceWindow.Settings.DefaultVideoDevice.SettingsButton.Tooltip="Настройки устройства видеозахвата" +MarketplaceWindow.Settings.EnableMakerTools.Tip="Экспортировать и импортировать коллекции сцен вручную" +MarketplaceWindow.Settings.EnableMakerTools.Tooltip="Активирует инструменты экспорта и импорта для авторов контента, которые хотят публиковать свои коллекции сцен в Elgato Marketplace." +MarketplaceWindow.Settings.EnableMakerTools="Включить Maker tools" +MarketplaceWindow.Settings.InstallLocation="Папка с медиафайлами" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Disabled="Экспорт и импорт станут недоступны после перезагрузки OBS Studio" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Enabled="Экспорт и импорт станут доступны после перезагрузки OBS Studio" +MarketplaceWindow.Settings.Title="Настройки" +MarketplaceWindow.StoreButton.Tooltip="Перейти в Elgato Marketplace" +SceneCollectionInfo.CloseButton="Закрыть" +SceneCollectionInfo.Text="Чтобы использовать эту коллекцию, загрузите и установите необходимые ресурсы, а затем перезапустите OBS Studio" +SceneCollectionInfo.Title="Для этой коллекции сцен необходимы сторонние ресурсы" +SceneCollectionInfo.WindowTitle="Сведения о коллекции сцен" +SetupWizard.AudioSetup.Device.Text="Выбор микрофона" +SetupWizard.BackButton="Назад" +SetupWizard.CreateCollection.NewNameLabel="Имя" +SetupWizard.CreateCollection.SubTitle="Коллекция будет отображаться под этим именем" +SetupWizard.CreateCollection.Title="Имя коллекции сцен" +SetupWizard.ImportTitlePrefix="Импорт коллекции" +SetupWizard.IncompatibleFile.Text="Произошел сбой: этот загруженный пакет не содержал корректного файла bundleInfo.json и не может быть установлен. (Проблема с файлом коллекции сцен, загруженным на сервер)" +SetupWizard.IncompatibleFile.Title="Несовместимый файл" +SetupWizard.InstallButton="Импорт" +SetupWizard.Loading.Text="Если коллекция содержит большие файлы, процесс может занять некоторое время" +SetupWizard.Loading.Title="Идет загрузка коллекции…" +SetupWizard.MergeCollection="Объединить с текущей" +SetupWizard.MergeCollectionName.SubTitle="Задайте имя для новой объединенной коллекции сцен" +SetupWizard.MergeCollectionName.Title="Имя объединенной коллекции сцен" +SetupWizard.MergeCollectionSteps.ChooseMicrophone="Выбор микрофона" +SetupWizard.MergeCollectionSteps.GetStarted="Первые шаги" +SetupWizard.MergeCollectionSteps.SelectScenes="Выбор сцен" +SetupWizard.MergeCollectionSteps.SetUpCameras="Настройка камер" +SetupWizard.MergeSelectScenes.AllScenes.Tooltip="Если установлен этот флажок, все сцены в коллекции будут объединены с текущей коллекцией" +SetupWizard.MergeSelectScenes.AllScenes="Импортировать все сцены" +SetupWizard.MergeSelectScenes.NoCustomMergeAvailable="Эта коллекция сцен позволяет только объединение всех сцен сразу" +SetupWizard.MergeSelectScenes.SubTitle="Все выбранные сцены (и зависимые элементы) будут добавлены к текущей коллекции сцен" +SetupWizard.MergeSelectScenes.Title="Выберите сцены, которые вы хотите объединить" +SetupWizard.MissingPlugins.DownloadButton="Загрузить" +SetupWizard.MissingPlugins.Text="Загрузите и установите необходимые плагины, перезапустите OBS и повторите попытку" +SetupWizard.MissingPlugins.Title.Plural="Для этой коллекции сцен требуются плагины ({COUNT})" +SetupWizard.MissingPlugins.Title.Single="Для этой коллекции сцен требуется плагин" +SetupWizard.MissingSourceClone.Description="Загрузите и установите плагин, перезапустите OBS Studio и повторите попытку" +SetupWizard.MissingSourceClone.Title="Для объединения коллекций сцен требуется плагин Source Clone" +SetupWizard.NameInUseError.Text="Коллекция сцен с таким именем уже существует. Выберите другое имя." +SetupWizard.NameInUseError.Title="Имя уже занято" +SetupWizard.NewCollection="Создать" +SetupWizard.NewCollectionSteps.ChooseMicrophone="Выбор микрофона" +SetupWizard.NewCollectionSteps.GetStarted="Первые шаги" +SetupWizard.NewCollectionSteps.NameSceneCollection="Имя коллекции сцен" +SetupWizard.NewCollectionSteps.SetUpCameras="Настройка камер" +SetupWizard.NextButton="Продолжить" +SetupWizard.SelectInstall.ExistingInstallButton="Добавить" +SetupWizard.SelectInstall.NewInstallButton="Создать" +SetupWizard.SelectInstall.Title="Какой вариант установки вы предпочитаете?" +SetupWizard.StartInstallation.Description="Для начала давайте дадим коллекции сцен имя и выберем устройства ввода" +SetupWizard.VideoSetup.SubTitle.Plural="Привязка камер к источникам видео" +SetupWizard.VideoSetup.SubTitle.Singular="Привязка камеры к источнику видео" +SetupWizard.VideoSetup.Title.Plural="Найдено источников видео: {COUNT}" +SetupWizard.VideoSetup.Title.Singular="Найден 1 источник видео" +SetupWizard.WindowTitle="Импорт коллекции сцен из Elgato Marketplace" +UpdateModal.Description="Установите последнюю версию Elgato Marketplace Connect, чтобы использовать все новинки и доработки" +UpdateModal.DownloadUpdateButton="Обновить сейчас" +UpdateModal.LaterButton="Напомнить позже" +UpdateModal.SkipVersionButton="Нет, спасибо" +UpdateModal.Title="Доступно обновление" diff --git a/data/locale/sv-SE.ini b/data/locale/sv-SE.ini new file mode 100644 index 0000000..e6549b8 --- /dev/null +++ b/data/locale/sv-SE.ini @@ -0,0 +1,129 @@ +AssetProtection.Message="Denna scensamling innehåller element från ett köp som gjorts på Elgato Marketplace. För att installera den behöver du logga in med kontot som användes för att göra köpet. Du kan logga in genom Elgato Marketplace-menyn och sedan försöka importera igen." +ExportWizard.BackButton="Tillbaka" +ExportWizard.CloseButton="Klart" +ExportWizard.Complete="Export av {COLLECTION_NAME} slutförd" +ExportWizard.Export="Exportera" +ExportWizard.ExportComplete.Text="Din scensamling är nu redo att testas eller laddas upp till Marketplace" +ExportWizard.Exporting.Text="Det kan ta några minuter för scensamlingar med stora filer" +ExportWizard.Exporting.Title="Exporterar paket…" +ExportWizard.ExportTitle="Exportera {COLLECTION_NAME}" +ExportWizard.GetStartedButton="Ställ in" +ExportWizard.MediaFileCheck.SubtitleNone="0 mediefiler inkluderas med scensamlingen" +ExportWizard.MediaFileCheck.SubtitlePlural="Dessa filer inkluderas med scensamlingen" +ExportWizard.MediaFileCheck.SubtitleSingular="Denna fil inkluderas med scensamlingen" +ExportWizard.MediaFileCheck.Title="Granska exporten" +ExportWizard.MediaFileCheck.TitlePlural="{COUNT} mediefiler hittades" +ExportWizard.MediaFileCheck.TitleSingular="1 mediefil hittades" +ExportWizard.NextButton="Nästa" +ExportWizard.OutputScenes.Subtitle="Undertitel för utmatningsscener" +ExportWizard.OutputScenes.Title="Välj utmatningsscener" +ExportWizard.RequiredPlugins.NoPluginsFound="Hittade inga installerade insticksfiler" +ExportWizard.RequiredPlugins.PluralTitle="{COUNT} insticksfiler hittades" +ExportWizard.RequiredPlugins.SingleTitle="1 insticksfil hittades" +ExportWizard.RequiredPlugins.SubTitle="Välj de insticksfiler som krävs för den här scensamlingen" +ExportWizard.StartExport.Description="Låt oss först samla in beroenden och ställa in källor" +ExportWizard.Steps.BundleRequiredPlugins="Samla insticksfilerna som krävs" +ExportWizard.Steps.CollectMediaFiles="Samla in mediefiler" +ExportWizard.Steps.GetStarted="Kom igång" +ExportWizard.Steps.RenameVideoSources="Byt namn på videokällor" +ExportWizard.Steps.SelectOutputScenes="Välj utmatningsscener" +ExportWizard.Steps.ThirdPartyRequirements="Tredjepartskrav" +ExportWizard.ThirdPartyRequirements.SubTitle="Ange en lista med namn och webbadresser för eventuella tredjepartskrav." +ExportWizard.ThirdPartyRequirements.Title="Tredjepartskrav" +ExportWizard.VideoSourceLabel.CurrentLabel="Aktuellt" +ExportWizard.VideoSourceLabel.NewLabel="Nytt" +ExportWizard.VideoSourceLabel.SubTitle="Ställ in hur videokällor visas för användare i deras lista med källor" +ExportWizard.VideoSourceLabels.InputPlaceholder="Beskrivningstexten ska stå här" +ExportWizard.VideoSourceLabels.NoCaptureSourcesSub="Inga videokällor inkluderas med scensamlingen" +ExportWizard.VideoSourceLabels.NoCaptureSourcesText="Inga videokällor hittades" +ExportWizard.VideoSourceLabels.Title="Byt namn på videokällor" +General.CancelButton="Avbryt" +General.SaveButton="Spara" +MarketplaceWindow.ConnectionError.Subtitle="Kunde inte ansluta till servern (kontrollera nätverksanslutningen)." +MarketplaceWindow.ConnectionError.Title="Anslutningsfel" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Subtitle="Filen är inte en .elgatoscene-fil" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Title="Ogiltig filtyp" +MarketplaceWindow.DownloadProduct.NetworkError.Subtitle="Kunde inte ansluta till Marketplace. Försök igen." +MarketplaceWindow.DownloadProduct.NetworkError.Title="Nätverksanslutningsfel" +MarketplaceWindow.Loading.Title="Läser in dina köpta produkter…" +MarketplaceWindow.LoggingIn.Subtitle="Fungerar det inte? Klicka på Försök igen för att öppna på nytt i webbläsaren" +MarketplaceWindow.LoggingIn.Title="Tar dig till webbläsaren för att logga in" +MarketplaceWindow.LoggingIn.TryAgain="Försök igen" +MarketplaceWindow.LoginButton.LogIn="Logga in" +MarketplaceWindow.LoginError.Subtitle="Ett fel inträffade vid inloggningen. Försök logga in igen för att fortsätta." +MarketplaceWindow.LoginError.Title="Logga in" +MarketplaceWindow.LoginNeeded.Subtitle="Låt oss först ansluta ditt Marketplace-konto till OBS Studio" +MarketplaceWindow.LoginNeeded.Title="Logga in för att komma igång" +MarketplaceWindow.OpenSettingsButton.Tooltip="Öppna inställningar" +MarketplaceWindow.Purchased.NoPurchasesSubtitle="Dina digitala resurser från Marketplace visas här" +MarketplaceWindow.Purchased.NoPurchasesTitle="Du äger inte några scensamlingsprodukter ännu" +MarketplaceWindow.Purchased.OpenMarketplaceButton="Utforska Marketplace" +MarketplaceWindow.PurchasedTab="Ditt bibliotek" +MarketplaceWindow.Settings.Advanced="Avancerade" +MarketplaceWindow.Settings.DefaultAudioDevice.Label="Mikrofon" +MarketplaceWindow.Settings.DefaultAV.Label="Förvalda inmatningsenheter" +MarketplaceWindow.Settings.DefaultVideoDevice.Label="Kamera" +MarketplaceWindow.Settings.DefaultVideoDevice.SettingsButton.Tooltip="Inställningar för videoinsamlingsenheter" +MarketplaceWindow.Settings.EnableMakerTools.Tip="Exportera och importera scensamlingar manuellt" +MarketplaceWindow.Settings.EnableMakerTools.Tooltip="Aktiverar export- och importverktyg för kreatörer som vill skapa scensamlingar till Elgato Marketplace." +MarketplaceWindow.Settings.EnableMakerTools="Aktivera kreatörsverktyg" +MarketplaceWindow.Settings.InstallLocation="Mapp med mediefiler" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Disabled="När OBS Studio har startats om kommer export och import inte att vara tillgängliga längre" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Enabled="När OBS Studio har startats om kommer export och import att vara tillgängliga" +MarketplaceWindow.Settings.Title="Inställningar" +MarketplaceWindow.StoreButton.Tooltip="Gå till Elgato Marketplace" +SceneCollectionInfo.CloseButton="Stäng" +SceneCollectionInfo.Text="Hämta och installera resurserna som krävs och starta sedan om OBS Studio för att använda den här samlingen" +SceneCollectionInfo.Title="Tredjepartsresurser krävs för den här scensamlingen" +SceneCollectionInfo.WindowTitle="Info om scensamling" +SetupWizard.AudioSetup.Device.Text="Välj en mikrofon" +SetupWizard.BackButton="Tillbaka" +SetupWizard.CreateCollection.NewNameLabel="Namn" +SetupWizard.CreateCollection.SubTitle="Detta namn visas i listan med scensamlingar" +SetupWizard.CreateCollection.Title="Ange namn på scensamling" +SetupWizard.ImportTitlePrefix="Importera" +SetupWizard.IncompatibleFile.Text="Fel: den här hämtningen innehöll inte en giltig bundleInfo.json-fil och kan inte installeras (det finns ett problem med den inskickade scensamlingsfilen på servern)." +SetupWizard.IncompatibleFile.Title="Inkompatibel fil" +SetupWizard.InstallButton="Importera" +SetupWizard.Loading.Text="Det kan ta en stund för scensamlingar med stora filer" +SetupWizard.Loading.Title="Läser in samling…" +SetupWizard.MergeCollection="Slå ihop med aktuell" +SetupWizard.MergeCollectionName.SubTitle="Ge ett namn till den nya hopslagna scensamlingen" +SetupWizard.MergeCollectionName.Title="Ange namn på hopslagen scensamling" +SetupWizard.MergeCollectionSteps.ChooseMicrophone="Välj mikrofon" +SetupWizard.MergeCollectionSteps.GetStarted="Kom igång" +SetupWizard.MergeCollectionSteps.SelectScenes="Välj scener" +SetupWizard.MergeCollectionSteps.SetUpCameras="Ställ in kameror" +SetupWizard.MergeSelectScenes.AllScenes.Tooltip="Om du markerar alternativet så slås alla scener som ingår i scensamlingen ihop med den aktuella samlingen" +SetupWizard.MergeSelectScenes.AllScenes="Importera alla scener" +SetupWizard.MergeSelectScenes.NoCustomMergeAvailable="För denna scensamling är det endast möjligt att slå ihop alla scener på en gång" +SetupWizard.MergeSelectScenes.SubTitle="Alla valda scener (och beroenden) läggs till i den aktuella scensamlingen" +SetupWizard.MergeSelectScenes.Title="Välj scener som ska slås ihop" +SetupWizard.MissingPlugins.DownloadButton="Hämta" +SetupWizard.MissingPlugins.Text="Hämta och installera de insticksfiler som krävs och starta sedan om OBS och försök igen" +SetupWizard.MissingPlugins.Title.Plural="{COUNT} insticksfiler behövs för den här scensamlingen" +SetupWizard.MissingPlugins.Title.Single="1 insticksfil behövs för den här scensamlingen" +SetupWizard.MissingSourceClone.Description="Hämta och installera insticksfilen och starta sedan om OBS Studio och försök igen" +SetupWizard.MissingSourceClone.Title="Insticksfilen Source Clone krävs för att slå ihop scensamlingar" +SetupWizard.NameInUseError.Text="Det finns redan en scensamling med det här namnet. Ange ett annat namn." +SetupWizard.NameInUseError.Title="Namn som används" +SetupWizard.NewCollection="Skapa ny" +SetupWizard.NewCollectionSteps.ChooseMicrophone="Välj mikrofon" +SetupWizard.NewCollectionSteps.GetStarted="Kom igång" +SetupWizard.NewCollectionSteps.NameSceneCollection="Ange namn på scensamling" +SetupWizard.NewCollectionSteps.SetUpCameras="Ställ in kameror" +SetupWizard.NextButton="Fortsätt" +SetupWizard.SelectInstall.ExistingInstallButton="Lägg till" +SetupWizard.SelectInstall.NewInstallButton="Ny" +SetupWizard.SelectInstall.Title="Hur vill du installera denna?" +SetupWizard.StartInstallation.Description="Låt oss först ange ett namn på scensamlingen och välja dina inmatningskällor" +SetupWizard.VideoSetup.SubTitle.Plural="Tilldela kameror till videokällorna" +SetupWizard.VideoSetup.SubTitle.Singular="Tilldela en kamera till videokällan" +SetupWizard.VideoSetup.Title.Plural="{COUNT} videokällor hittades" +SetupWizard.VideoSetup.Title.Singular="1 videokälla hittades" +SetupWizard.WindowTitle="Importera scensamling från Elgato Marketplace" +UpdateModal.Description="Hämta senaste versionen av Elgato Marketplace Connect för att komma åt nya funktioner och förbättringar" +UpdateModal.DownloadUpdateButton="Uppdatera nu" +UpdateModal.LaterButton="Fråga mig senare" +UpdateModal.SkipVersionButton="Nej tack" +UpdateModal.Title="Uppdatering tillgänglig" diff --git a/data/locale/zh-TW.ini b/data/locale/zh-TW.ini new file mode 100644 index 0000000..53408b2 --- /dev/null +++ b/data/locale/zh-TW.ini @@ -0,0 +1,129 @@ +AssetProtection.Message="這個場景集合包含從 Elgato Marketplace 購買的內容。若要安裝,請使用購買時所用的帳號登入。您可以透過 Elgato Marketplace 選單登入,然後再試著重新匯入。" +ExportWizard.BackButton="返回" +ExportWizard.CloseButton="完成" +ExportWizard.Complete="「{COLLECTION_NAME}」匯出完成" +ExportWizard.Export="匯出" +ExportWizard.ExportComplete.Text="您的場景集合已可供測試或上傳至 Marketplace" +ExportWizard.Exporting.Text="場景集合若有較大的檔案,可能會需要幾分鐘" +ExportWizard.Exporting.Title="匯出套裝⋯" +ExportWizard.ExportTitle="匯出「{COLLECTION_NAME}」" +ExportWizard.GetStartedButton="設定" +ExportWizard.MediaFileCheck.SubtitleNone="此場景集合內包含 0 個媒體檔案" +ExportWizard.MediaFileCheck.SubtitlePlural="此場景集合內包含這些媒體檔案" +ExportWizard.MediaFileCheck.SubtitleSingular="此場景集合內包含這個媒體檔案" +ExportWizard.MediaFileCheck.Title="查看匯出" +ExportWizard.MediaFileCheck.TitlePlural="找到 {COUNT} 個媒體檔案" +ExportWizard.MediaFileCheck.TitleSingular="找到 1 個媒體檔案" +ExportWizard.NextButton="下一步" +ExportWizard.OutputScenes.Subtitle="輸出場景字幕" +ExportWizard.OutputScenes.Title="選擇輸出場景" +ExportWizard.RequiredPlugins.NoPluginsFound="沒有找到已安裝的外掛程式" +ExportWizard.RequiredPlugins.PluralTitle="找到 {COUNT} 個外掛程式" +ExportWizard.RequiredPlugins.SingleTitle="找到 1 個外掛程式" +ExportWizard.RequiredPlugins.SubTitle="選擇此場景集合需要的外掛程式" +ExportWizard.StartExport.Description="首先,我們先收集相依內容並設定來源" +ExportWizard.Steps.BundleRequiredPlugins="套裝需要外掛程式" +ExportWizard.Steps.CollectMediaFiles="收集媒體檔案" +ExportWizard.Steps.GetStarted="開始使用" +ExportWizard.Steps.RenameVideoSources="重新命名影片來源" +ExportWizard.Steps.SelectOutputScenes="選擇輸出場景" +ExportWizard.Steps.ThirdPartyRequirements="第三方要求" +ExportWizard.ThirdPartyRequirements.SubTitle="請提供任何第三方要求的名稱與網址。" +ExportWizard.ThirdPartyRequirements.Title="第三方要求" +ExportWizard.VideoSourceLabel.CurrentLabel="目前名稱" +ExportWizard.VideoSourceLabel.NewLabel="新名稱" +ExportWizard.VideoSourceLabel.SubTitle="設定影片來源在使用者來源列表中顯示的方式。" +ExportWizard.VideoSourceLabels.InputPlaceholder="描述文字會顯示於此" +ExportWizard.VideoSourceLabels.NoCaptureSourcesSub="此場景集合內不會包含媒體檔案" +ExportWizard.VideoSourceLabels.NoCaptureSourcesText="沒有找到影片來源" +ExportWizard.VideoSourceLabels.Title="重新命名影片來源" +General.CancelButton="取消" +General.SaveButton="儲存" +MarketplaceWindow.ConnectionError.Subtitle="無法連結至伺服器。(請檢查您的網路連線)" +MarketplaceWindow.ConnectionError.Title="連線錯誤" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Subtitle="檔案不是 .elgatoscene 檔" +MarketplaceWindow.DownloadProduct.InvalidFiletype.Title="檔案類型無效" +MarketplaceWindow.DownloadProduct.NetworkError.Subtitle="無法連結至 Marketplace。請再試一次。" +MarketplaceWindow.DownloadProduct.NetworkError.Title="網路連線錯誤" +MarketplaceWindow.Loading.Title="正在載入您購買的商品⋯" +MarketplaceWindow.LoggingIn.Subtitle="還是無法登入嗎?請點擊「再試一次」以重新啟動瀏覽器" +MarketplaceWindow.LoggingIn.Title="正將您導向瀏覽器登入" +MarketplaceWindow.LoggingIn.TryAgain="再試一次" +MarketplaceWindow.LoginButton.LogIn="登入" +MarketplaceWindow.LoginError.Subtitle="登入遇到問題。請再登入一次以繼續。" +MarketplaceWindow.LoginError.Title="登入" +MarketplaceWindow.LoginNeeded.Subtitle="首先,我們來將您的 Marketplace 帳號與 OBS Studio 連結" +MarketplaceWindow.LoginNeeded.Title="登入即可開始" +MarketplaceWindow.OpenSettingsButton.Tooltip="開啟設定" +MarketplaceWindow.Purchased.NoPurchasesSubtitle="您的 Marketplace 數位素材會於此顯示" +MarketplaceWindow.Purchased.NoPurchasesTitle="您目前還沒有任何場景集合產品" +MarketplaceWindow.Purchased.OpenMarketplaceButton="探索 Marketplace" +MarketplaceWindow.PurchasedTab="您的媒體庫" +MarketplaceWindow.Settings.Advanced="進階設定" +MarketplaceWindow.Settings.DefaultAudioDevice.Label="麥克風" +MarketplaceWindow.Settings.DefaultAV.Label="預設輸入裝置" +MarketplaceWindow.Settings.DefaultVideoDevice.Label="攝影機" +MarketplaceWindow.Settings.DefaultVideoDevice.SettingsButton.Tooltip="影片拍攝裝置設定" +MarketplaceWindow.Settings.EnableMakerTools.Tip="手動匯出和匯入場景集合" +MarketplaceWindow.Settings.EnableMakerTools.Tooltip="想為 Elgato Marketplace 製作場景集合的創作者可使用的匯出和匯入工具。" +MarketplaceWindow.Settings.EnableMakerTools="啟用創作者工具" +MarketplaceWindow.Settings.InstallLocation="媒體檔案資料夾" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Disabled="重啟 OBS Studio 後就無法再使用匯出和匯入功能" +MarketplaceWindow.Settings.MakerToolsRestartWarning.Enabled="重啟 OBS Studio 後就可使用匯出和匯入功能" +MarketplaceWindow.Settings.Title="設定" +MarketplaceWindow.StoreButton.Tooltip="前往 Elgato Marketplace" +SceneCollectionInfo.CloseButton="關閉" +SceneCollectionInfo.Text="下載並安裝所需的素材,接著重啟 OBS Studio 以使用此集合" +SceneCollectionInfo.Title="此場景集合需要第三方素材" +SceneCollectionInfo.WindowTitle="場景集合資訊" +SetupWizard.AudioSetup.Device.Text="選擇麥克風" +SetupWizard.BackButton="返回" +SetupWizard.CreateCollection.NewNameLabel="名稱" +SetupWizard.CreateCollection.SubTitle="此名稱會顯示在您場景集合的清單中" +SetupWizard.CreateCollection.Title="為場景集合命名" +SetupWizard.ImportTitlePrefix="匯入" +SetupWizard.IncompatibleFile.Text="錯誤:此下載內容不含有效的 bundleInfo.json 檔案,因此無法安裝。(這個問題是來自提交至伺服器的場景集合檔案。)" +SetupWizard.IncompatibleFile.Title="不相容的檔案" +SetupWizard.InstallButton="匯入" +SetupWizard.Loading.Text="場景集合若有較大的檔案,可能會需要一些時間" +SetupWizard.Loading.Title="正在載入集合⋯" +SetupWizard.MergeCollection="與現有場景集合合併" +SetupWizard.MergeCollectionName.SubTitle="為新合併的場景集合命名" +SetupWizard.MergeCollectionName.Title="為合併的場景集合命名" +SetupWizard.MergeCollectionSteps.ChooseMicrophone="選擇麥克風" +SetupWizard.MergeCollectionSteps.GetStarted="開始使用" +SetupWizard.MergeCollectionSteps.SelectScenes="選擇場景" +SetupWizard.MergeCollectionSteps.SetUpCameras="設定攝影機" +SetupWizard.MergeSelectScenes.AllScenes.Tooltip="如勾選,此場景集合的所有場景就會被合併至目前的集合" +SetupWizard.MergeSelectScenes.AllScenes="匯入所有場景" +SetupWizard.MergeSelectScenes.NoCustomMergeAvailable="此場景集合只能一次合併所有場景" +SetupWizard.MergeSelectScenes.SubTitle="所有已選取的場景 (及相依內容) 都會新增至目前的場景集合" +SetupWizard.MergeSelectScenes.Title="選擇要合併的場景" +SetupWizard.MissingPlugins.DownloadButton="取得" +SetupWizard.MissingPlugins.Text="請下載並安裝必要的外掛程式,接著重啟 OBS 並再試一次" +SetupWizard.MissingPlugins.Title.Plural="此場景集合需要 {COUNT} 個外掛程式" +SetupWizard.MissingPlugins.Title.Single="此場景集合需要 1 個外掛程式" +SetupWizard.MissingSourceClone.Description="請下載並安裝該外掛程式,接著重啟 OBS Studio 並再試一次" +SetupWizard.MissingSourceClone.Title="需要 Source Clone 外掛程式才能合併場景集合" +SetupWizard.NameInUseError.Text="已有相同名稱的場景集合,請使用另一個名稱。" +SetupWizard.NameInUseError.Title="名稱已被使用" +SetupWizard.NewCollection="建立新場景集合" +SetupWizard.NewCollectionSteps.ChooseMicrophone="選擇麥克風" +SetupWizard.NewCollectionSteps.GetStarted="開始使用" +SetupWizard.NewCollectionSteps.NameSceneCollection="為場景集合命名" +SetupWizard.NewCollectionSteps.SetUpCameras="設定攝影機" +SetupWizard.NextButton="繼續" +SetupWizard.SelectInstall.ExistingInstallButton="新增" +SetupWizard.SelectInstall.NewInstallButton="新安裝" +SetupWizard.SelectInstall.Title="您想如何安裝?" +SetupWizard.StartInstallation.Description="首先,我們來為這個場景集合命名,並選擇輸入裝置" +SetupWizard.VideoSetup.SubTitle.Plural="指派攝影機至影片來源" +SetupWizard.VideoSetup.SubTitle.Singular="指派一台攝影機至影片來源" +SetupWizard.VideoSetup.Title.Plural="找到 {COUNT} 個影片來源" +SetupWizard.VideoSetup.Title.Singular="找到 1 個影片來源" +SetupWizard.WindowTitle="從 Elgato Marketplace 匯入場景集合" +UpdateModal.Description="取得最新版 Elgato Marketplace Connect,使用新功能與改善項目" +UpdateModal.DownloadUpdateButton="立即更新" +UpdateModal.LaterButton="稍後再說" +UpdateModal.SkipVersionButton="不用了,謝謝" +UpdateModal.Title="有可用更新" diff --git a/data/plugins.json b/data/plugins.json index c765925..031ffc6 100644 --- a/data/plugins.json +++ b/data/plugins.json @@ -146,6 +146,41 @@ "files" :[ "waveform.dll" ] + }, + { + "name": "Aitum Vertical", + "url": "https://obsproject.com/forum/resources/aitum-vertical.1715/", + "files": [ + "vertical-canvas.dll" + ] + }, + { + "name": "Markdown Source", + "url": "https://obsproject.com/forum/resources/markdown-source.1764/", + "files": [ + "markdown.dll" + ] + }, + { + "name": "Ashmanix Countdown Timer", + "url": "https://obsproject.com/forum/resources/ashmanix-countdown-timer.1610/", + "files": [ + "obs-plugin-countdown.dll" + ] + }, + { + "name": "SVG Source", + "url": "https://obsproject.com/forum/resources/svg-source.2174/", + "files": [ + "svg-source.dll" + ] + }, + { + "name": "Draw", + "url": "https://obsproject.com/forum/resources/draw.2081/", + "files": [ + "draw-dock.dll" + ] } ] } \ No newline at end of file diff --git a/loader/main.cpp b/loader/main.cpp index 0669a11..11c6b45 100644 --- a/loader/main.cpp +++ b/loader/main.cpp @@ -66,6 +66,7 @@ DWORD GetProcessIdFromExe(const std::wstring& exeName) { return 0; // Not found } + // Callback function to find the window associated with a given process ID BOOL CALLBACK WindowToForeground(HWND hwnd, LPARAM lParam) { DWORD processId; @@ -77,8 +78,14 @@ BOOL CALLBACK WindowToForeground(HWND hwnd, LPARAM lParam) { std::wstring title = windowTitle; // Bring the window to the foreground if (title.rfind(L"OBS ", 0) == 0) { // the main OBS window title starts with 'OBS ' + bool maximized = IsZoomed(hwnd); + bool minimized = IsIconic(hwnd); + if (!minimized) { + ShowWindow(hwnd, maximized ? SW_SHOWMAXIMIZED : SW_SHOW); + } else { + ShowWindow(hwnd, SW_RESTORE); + } SetForegroundWindow(hwnd); - ShowWindow(hwnd, SW_RESTORE); return FALSE; // Stop enumerating windows (we found the window) } } diff --git a/src/api.cpp b/src/api.cpp index f9d99fa..7af0f20 100644 --- a/src/api.cpp +++ b/src/api.cpp @@ -141,6 +141,7 @@ void MarketplaceApi::setUserDetails(nlohmann::json &data) try { _firstName = data.at("first_name").template get(); _lastName = data.at("last_name").template get(); + _id = data.at("id").template get(); std::string color = data.at("default_avatar_color") .template get(); _avatarColor = avatarColors.at(color); @@ -173,6 +174,7 @@ void MarketplaceApi::setUserDetails(nlohmann::json &data) } } _loggedIn = true; + emit UserDetailsUpdated(); } catch (...) { obs_log(LOG_ERROR, "There was a problem processing the user response data."); } @@ -231,4 +233,22 @@ void MarketplaceApi::OpenStoreInBrowser() const ShellExecuteA(NULL, NULL, url.c_str(), NULL, NULL, SW_SHOW); } +void MarketplaceApi::OpenAccountInBrowser() const +{ + auto ec = GetElgatoCloud(); + auto accessToken = ec->GetAccessToken(); + std::string storeUrl = + _storeUrl + + "/account/personal?utm_source=mp_connect&utm_medium=direct_software&utm_campaign=v_1.0"; + std::string url; + if (accessToken != "") { + std::string storeUrlEnc = url_encode(storeUrl); + url = _storeUrl + "/api/auth/login?token=" + accessToken + "&redirect=" + storeUrlEnc; + } + else { + url = storeUrl; + } + ShellExecuteA(NULL, NULL, url.c_str(), NULL, NULL, SW_SHOW); +} + } // namespace elgatocloud diff --git a/src/api.hpp b/src/api.hpp index f7cd0d3..ccc45a1 100644 --- a/src/api.hpp +++ b/src/api.hpp @@ -72,11 +72,13 @@ class MarketplaceApi : public QObject { inline std::string authUrl() const { return _authUrl; } inline std::string firstName() const { return _firstName; } inline std::string lastName() const { return _lastName; } + inline std::string id() const { return _id; } inline std::string avatarColor() const { return _avatarColor; } inline bool hasAvatar() const { return _hasAvatar; } inline std::string avatarUrl() const { return _avatarUrl; } inline bool avatarReady() const { return _avatarReady; } inline std::string avatarPath() const { return _avatarPath; } + inline bool loggedIn() const { return _loggedIn; } static void AvatarProgress(void* ptr, bool finished, bool downloading, uint64_t fileSize, uint64_t chunkSize, uint64_t downloaded); @@ -84,6 +86,7 @@ class MarketplaceApi : public QObject { void setUserDetails(nlohmann::json &data); void logOut(); void OpenStoreInBrowser() const; + void OpenAccountInBrowser() const; std::string getAuthUrl(std::vector const& segments, std::map const& queryParams); std::string getGatewayUrl( std::vector const& segments, @@ -92,6 +95,7 @@ class MarketplaceApi : public QObject { signals: void AvatarDownloaded(); + void UserDetailsUpdated(); private: MarketplaceApi(); @@ -111,6 +115,7 @@ class MarketplaceApi : public QObject { std::string _avatarColor; std::string _avatarUrl; std::string _avatarPath; + std::string _id; static MarketplaceApi *_api; static std::mutex _mtx; diff --git a/src/downloader.cpp b/src/downloader.cpp index 509aaae..0400078 100644 --- a/src/downloader.cpp +++ b/src/downloader.cpp @@ -130,30 +130,34 @@ size_t Downloader::DownloadEntry::handle_progress(void *ptr, curl_off_t dltotal, UNUSED_PARAMETER(ultotal); UNUSED_PARAMETER(ulnow); //double pct = (int)((double)dlnow / (double)dltotal * 100.0); - return CURL_PROGRESSFUNC_CONTINUE; + return self.cancel; } -Downloader::DownloadEntry::DownloadEntry(Downloader *parent, std::string url, - std::string targetPath, - ProgressCallbackFn pc, - CompleteCallbackFn cc, - void *callbackDat) +Downloader::DownloadEntry::DownloadEntry(Downloader* parent, std::string url, + std::string targetPath, + ProgressCallbackFn pc, + CompleteCallbackFn cc, + void* callbackDat) : url(url), - file(nullptr), - fileSize(0), - downloaded(0), - status(Downloader::Status::QUEUED), - references(0), - removed(false), - progressCallback(pc), - completeCallback(cc), - callbackData(callbackDat), - parent(parent) + file(nullptr), + fileSize(0), + downloaded(0), + status(Downloader::Status::QUEUED), + references(0), + removed(false), + progressCallback(pc), + completeCallback(cc), + callbackData(callbackDat), + parent(parent), + cancel(0) { handle = curl_easy_init(); curl_easy_setopt(handle, CURLOPT_URL, url.c_str()); curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, write_data); - curl_easy_setopt(handle, CURLOPT_WRITEDATA, static_cast(this)); + curl_easy_setopt(handle, CURLOPT_WRITEDATA, static_cast(this)); + curl_easy_setopt(handle, CURLOPT_XFERINFOFUNCTION, handle_progress); + curl_easy_setopt(handle, CURLOPT_XFERINFODATA, static_cast(this)); + curl_easy_setopt(handle, CURLOPT_NOPROGRESS, 0L); curl_easy_setopt(handle, CURLOPT_USERAGENT, "elgato-cloud 0.0"); curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, handle_header); curl_easy_setopt(handle, CURLOPT_HEADERDATA, static_cast(this)); @@ -210,10 +214,16 @@ Downloader::DownloadEntry::~DownloadEntry() void Downloader::DownloadEntry::Finish() { std::unique_lock l(lock); - status = Status::FINISHED; + status = status == Status::STOPPED ? Status::STOPPED : Status::FINISHED; curl_multi_remove_handle(parent->handle, handle); curl_easy_cleanup(handle); fclose(file); + + if (status == Status::STOPPED) { // User cancelled the download + os_unlink(tmpTargetName.c_str()); + return; + } + if (fileName == "") { fileName = detectedFileName; } @@ -226,11 +236,6 @@ void Downloader::DownloadEntry::Finish() char *absPath = os_get_abs_path_ptr(target.c_str()); target = std::string(absPath, strlen(absPath)); bfree(absPath); - - //absPath = os_get_abs_path_ptr(tmpTargetName.c_str()); - //tmpTargetName = std::string(absPath, strlen(absPath)); - //bfree(absPath); - parent->moveRequests.push_back( {tmpTargetName, target.c_str(), callbackData, completeCallback}); } @@ -410,6 +415,7 @@ void Downloader::fillEntry(Downloader::Entry &dst, dst.fileSize = src.fileSize; dst.downloaded = src.downloaded; dst.status = src.status; + dst.parent = src.parent; if (src.downloadedHistory.size() > 1) { auto first = src.downloadedHistory.front(); @@ -419,7 +425,7 @@ void Downloader::fillEntry(Downloader::Entry &dst, last.first - first.first) .count(); auto bytesDownloaded = last.second - first.second; - dst.speedBps = bytesDownloaded / interval; + dst.speedBps = interval != 0 ? bytesDownloaded / interval : 0; } } @@ -434,12 +440,18 @@ void Downloader::Entry::Update() void Downloader::Entry::Stop() { + if (!parent) { + return; + } std::unique_lock l(parent->lock); auto dlentry = parent->queue.find(id); if (dlentry != parent->queue.end()) { if (dlentry->second->status == Status::DOWNLOADING) { - curl_multi_remove_handle(parent->handle, - dlentry->second->handle); + dlentry->second->progressCallback = nullptr; + dlentry->second->completeCallback = nullptr; + dlentry->second->cancel = 1; + //curl_multi_remove_handle(parent->handle, + // dlentry->second->handle); dlentry->second->status = Status::STOPPED; } } @@ -474,6 +486,9 @@ void Downloader::tryDelete(decltype(Downloader::queue)::iterator iter) Downloader::Entry::~Entry() { + if (!parent) { + return; + } std::unique_lock l(parent->lock); auto dlentry = parent->queue.find(id); if (dlentry != parent->queue.end()) { diff --git a/src/downloader.h b/src/downloader.h index 5ca03ba..ddb6c30 100644 --- a/src/downloader.h +++ b/src/downloader.h @@ -83,6 +83,7 @@ class Downloader { Downloader *parent; ProgressCallbackFn progressCallback; CompleteCallbackFn completeCallback; + int cancel; void *callbackData; DownloadEntry(Downloader *parent, std::string url, @@ -132,7 +133,7 @@ class Downloader { public: struct Entry { size_t id; - Downloader *parent; + Downloader *parent = nullptr; std::string fileName, url; uint64_t fileSize, downloaded, speedBps; // bytes per second diff --git a/src/elgato-cloud-config.cpp b/src/elgato-cloud-config.cpp index 0bba367..b7b71bc 100644 --- a/src/elgato-cloud-config.cpp +++ b/src/elgato-cloud-config.cpp @@ -15,6 +15,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ +#include #include #include @@ -32,7 +33,7 @@ with this program. If not, see #include #include #include -#include +#include #include #include #include @@ -52,34 +53,37 @@ DefaultAVWidget::DefaultAVWidget(QWidget *parent) : QWidget(parent) AVWIDGET = this; std::string imageBaseDir = GetDataPath(); imageBaseDir += "/images/"; - auto layout = new QHBoxLayout(); + layout->setSpacing(16); + layout->setContentsMargins(0, 0, 0, 0); auto dropDowns = new QVBoxLayout(); + dropDowns->setSpacing(16); + dropDowns->setContentsMargins(0, 0, 0, 0); + + auto avWidgetTitle = new QLabel(obs_module_text("MarketplaceWindow.Settings.DefaultAV.Label"), this); + avWidgetTitle->setStyleSheet(EWizardStepTitle); auto videoSourceLabel = new QLabel( obs_module_text("MarketplaceWindow.Settings.DefaultVideoDevice.Label"), this); - videoSourceLabel->setStyleSheet("font-size: 12pt;"); + videoSourceLabel->setStyleSheet(EWizardFieldLabel); auto audioSourceLabel = new QLabel( obs_module_text("MarketplaceWindow.Settings.DefaultAudioDevice.Label"), this); - audioSourceLabel->setStyleSheet("font-size: 12pt;"); + audioSourceLabel->setStyleSheet(EWizardFieldLabel); _videoSources = new QComboBox(this); - _videoSources->setStyleSheet(EComboBoxStyle); + _videoSources->setStyleSheet(EWizardComboBoxStyle); _videoPreview = new OBSQTDisplay(this); - _videoPreview->setFixedHeight(171); - _videoPreview->hide(); + auto videoPreviewWidget = new VideoPreviewWidget(_videoPreview, 8, this); + videoPreviewWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - _blank = new QLabel(this); - _blank->setText( - obs_module_text("MarketplaceWindow.Settings.DefaultVideoDevice.NoneSelected")); - _blank->setAlignment(Qt::AlignCenter); - _blank->setFixedHeight(171); - _blank->setFixedWidth(304); + _blank = new CameraPlaceholder(8, this); + std::string cameraIconPath = imageBaseDir + "camera-placeholder-icon.svg"; + _blank->setIcon(cameraIconPath.c_str()); + _blank->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - //auto videoSettings = new QHBoxLayout(this); auto videoSettings = new QWidget(this); auto vsLayout = new QHBoxLayout(this); vsLayout->setContentsMargins(0, 0, 0, 0); @@ -87,18 +91,13 @@ DefaultAVWidget::DefaultAVWidget(QWidget *parent) : QWidget(parent) vsLayout->addWidget(_videoSources); _settingsButton = new QPushButton(this); - std::string settingsIconPath = imageBaseDir + "settings.svg"; - std::string settingsIconHoverPath = imageBaseDir + "settings_hover.svg"; - std::string settingsIconDisabledPath = - imageBaseDir + "settings_disabled.svg"; - QString settingsButtonStyle = EIconHoverDisabledButtonStyle; + std::string settingsIconPath = imageBaseDir + "button-settings-icon.svg"; + std::string settingsIconDisabledPath = imageBaseDir + "button-settings-icon-disabled.svg"; + QString settingsButtonStyle = EWizardIconOnlyButtonStyle; settingsButtonStyle.replace("${img}", settingsIconPath.c_str()); - settingsButtonStyle.replace("${hover-img}", - settingsIconHoverPath.c_str()); - settingsButtonStyle.replace("${disabled-img}", - settingsIconDisabledPath.c_str()); - _settingsButton->setFixedSize(24, 24); - _settingsButton->setMaximumHeight(24); + settingsButtonStyle.replace("${img-disabled}", settingsIconDisabledPath.c_str()); + _settingsButton->setFixedSize(32, 32); + _settingsButton->setMaximumHeight(32); _settingsButton->setStyleSheet(settingsButtonStyle); _settingsButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); _settingsButton->setToolTip( @@ -110,18 +109,30 @@ DefaultAVWidget::DefaultAVWidget(QWidget *parent) : QWidget(parent) videoSettings->setLayout(vsLayout); videoSettings->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); - dropDowns->addWidget(videoSourceLabel); - dropDowns->addWidget(videoSettings); - dropDowns->addWidget(audioSourceLabel); _audioSources = new QComboBox(this); - _audioSources->setStyleSheet(EComboBoxStyle); + _audioSources->setStyleSheet(EWizardComboBoxStyle); _levelsWidget = new SimpleVolumeMeter(this, _volmeter); // Add Dropdown and meter - dropDowns->addWidget(_audioSources); - dropDowns->addWidget(_levelsWidget); + dropDowns->addWidget(avWidgetTitle); + auto videoSourceSettingLayout = new QVBoxLayout(); + videoSourceSettingLayout->setContentsMargins(0, 0, 0, 0); + videoSourceSettingLayout->setSpacing(0); + videoSourceSettingLayout->addWidget(videoSourceLabel); + videoSourceSettingLayout->addWidget(videoSettings); + dropDowns->addLayout(videoSourceSettingLayout); + + auto audioSourceSettingLayout = new QVBoxLayout(); + audioSourceSettingLayout->setContentsMargins(0, 0, 0, 0); + audioSourceSettingLayout->setSpacing(0); + audioSourceSettingLayout->addWidget(audioSourceLabel); + audioSourceSettingLayout->addWidget(_audioSources); + + dropDowns->addLayout(audioSourceSettingLayout); + dropDowns->addStretch(); + //dropDowns->addWidget(_levelsWidget); // Get settings auto config = elgatoCloud->GetConfig(); @@ -167,12 +178,18 @@ DefaultAVWidget::DefaultAVWidget(QWidget *parent) : QWidget(parent) //avSettings->addWidget(_videoPreview); _stack = new QStackedWidget(this); - _stack->setFixedSize(300, 150); + _stack->setFixedWidth(300); _stack->addWidget(_blank); - _stack->addWidget(_videoPreview); - _stack->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + _stack->addWidget(videoPreviewWidget); + _stack->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + auto previewsLayout = new QVBoxLayout(); + previewsLayout->setContentsMargins(0, 0, 0, 0); + previewsLayout->setSpacing(8); + previewsLayout->addWidget(_stack); + previewsLayout->addWidget(_levelsWidget); + previewsLayout->addStretch(); layout->addLayout(dropDowns); - layout->addWidget(_stack); + layout->addLayout(previewsLayout); setLayout(layout); @@ -241,7 +258,6 @@ DefaultAVWidget::~DefaultAVWidget() } if (_videoCaptureSource) { obs_source_release(_videoCaptureSource); - //obs_source_remove(_videoCaptureSource); } _levelsWidget = nullptr; } @@ -404,7 +420,7 @@ SimpleVolumeMeter::SimpleVolumeMeter(QWidget *parent, obs_volmeter_t *volmeter) : QWidget(parent), _volmeter(volmeter) { - setFixedHeight(16); + setFixedHeight(8); } SimpleVolumeMeter::~SimpleVolumeMeter() {} @@ -427,7 +443,7 @@ void SimpleVolumeMeter::calculateDisplayPeak(uint64_t ts) _displayPeak = _currentPeak; } else { float decay = deltaT * _decayRate; - _displayPeak = std::max(_displayPeak - decay, _minMag); + _displayPeak = (std::max)(_displayPeak - decay, _minMag); } } @@ -443,16 +459,26 @@ void SimpleVolumeMeter::paintEvent(QPaintEvent *event) float fwidth = static_cast(width) * (_displayPeak - _minMag) / (-_minMag); int newWidth = static_cast(fwidth); - widgetRect.setWidth(std::min(width, newWidth)); + widgetRect.setWidth((std::min)(width, newWidth)); QPainter painter(this); - painter.fillRect(bgRect, QColor(50, 50, 50)); + QPainterPath path; + path.addRoundedRect(bgRect, 4, 4); + painter.setClipPath(path); + // Draw background + painter.fillRect(bgRect, QColor(49, 49, 49)); QLinearGradient gradient(0, 0, width, 0); - gradient.setColorAt(0.0, QColor(86, 69, 255)); - gradient.setColorAt(1.0, QColor(129, 60, 255)); - painter.fillRect(widgetRect, gradient); + gradient.setColorAt(0.0, QColor(59, 180, 85)); + gradient.setColorAt(0.6, QColor(59, 180, 85)); + gradient.setColorAt(0.8, QColor(251, 219, 0)); + gradient.setColorAt(1.0, QColor(255, 60, 78)); + path.clear(); + path.addRoundedRect(widgetRect, 4, 4); + painter.setClipPath(path); + + painter.fillRect(bgRect, gradient); _lastRedraw = ts; } @@ -473,42 +499,51 @@ ElgatoCloudConfig::ElgatoCloudConfig(QWidget *parent) : QDialog(parent) std::string imageBaseDir = GetDataPath(); imageBaseDir += "/images/"; - setFixedSize(QSize(680, 500)); + setFixedSize(QSize(680, 528)); setAttribute(Qt::WA_DeleteOnClose); + setWindowTitle(obs_module_text("MarketplaceWindow.Settings.Title")); auto layout = new QVBoxLayout(); - - auto title = new QLabel(this); - title->setText( - obs_module_text("MarketplaceWindow.Settings.Title")); - title->setAlignment(Qt::AlignCenter); - title->setStyleSheet("QLabel { font-size: 16pt; }"); - title->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - - layout->addWidget(title); + layout->setSpacing(16); + layout->setContentsMargins(16, 16, 16, 16); // Default video and audio capture device settings _avWidget = new DefaultAVWidget(this); layout->addWidget(_avWidget); + // Create the horizontal line + QFrame* line = new QFrame(); + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Plain); // No 3D effect + line->setLineWidth(1); + line->setFixedHeight(1); // Force it to stay 1px high + line->setStyleSheet("color: rgba(255, 255, 255, 0.12); rgba(255, 255, 255, 0.12);"); + + layout->addWidget(line); + + auto advancedTitle = new QLabel(obs_module_text("MarketplaceWindow.Settings.Advanced"), this); + advancedTitle->setStyleSheet(EWizardStepTitle); + + layout->addWidget(advancedTitle); + // Theme installation location setting auto filePickerLabel = new QLabel( obs_module_text("MarketplaceWindow.Settings.InstallLocation"), this); - filePickerLabel->setStyleSheet("margin-left: 16px; font-size: 12pt;"); + filePickerLabel->setStyleSheet(EWizardFieldLabel); auto config = elgatoCloud->GetConfig(); _installDirectory = obs_data_get_string(config, "InstallLocation"); auto filePicker = new QWidget(this); filePicker->setStyleSheet( - "QWidget {background-color: #181818; border-radius: 8px; margin: 0px 8px 0px 8px; padding: 0px 16px 0px 0px;}"); + "QWidget {background-color: #232323; border-radius: 8px; margin: 0px 0px 0px 0px; padding: 0px 16px 0px 0px;}"); auto fpLayout = new QHBoxLayout(); fpLayout->setSpacing(0); fpLayout->setContentsMargins(0, 0, 0, 0); auto directory = new QLabel(_installDirectory.c_str(), filePicker); - directory->setFixedHeight(40); + directory->setFixedHeight(32); directory->setStyleSheet( - "QLabel {color: #FFFFFF; padding: 0px 16px 0px 16px; font-size: 11pt;}"); + "QLabel {color: #FFFFFF; padding: 0px 16px 0px 16px; font-size: 14px;}"); directory->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); auto directoryPick = new QPushButton(filePicker); @@ -543,8 +578,13 @@ ElgatoCloudConfig::ElgatoCloudConfig(QWidget *parent) : QDialog(parent) fpLayout->addWidget(directoryPick); filePicker->setLayout(fpLayout); - layout->addWidget(filePickerLabel); - layout->addWidget(filePicker); + auto filePickerLayout = new QVBoxLayout(); + filePickerLayout->setContentsMargins(0, 0, 0, 0); + filePickerLayout->setSpacing(4); + + filePickerLayout->addWidget(filePickerLabel); + filePickerLayout->addWidget(filePicker); + layout->addLayout(filePickerLayout); // Maker Tools toggle. bool makerTools = obs_data_get_bool(config, "MakerTools"); @@ -553,12 +593,21 @@ ElgatoCloudConfig::ElgatoCloudConfig(QWidget *parent) : QDialog(parent) _makerCheckbox->setChecked(makerTools); std::string checkedImage = imageBaseDir + "checkbox_checked.png"; std::string uncheckedImage = imageBaseDir + "checkbox_unchecked.png"; - QString checkBoxStyle = ECheckBoxStyle; + QString checkBoxStyle = EWizardCheckBoxStyle; checkBoxStyle.replace("${checked-img}", checkedImage.c_str()); checkBoxStyle.replace("${unchecked-img}", uncheckedImage.c_str()); _makerCheckbox->setStyleSheet(checkBoxStyle); _makerCheckbox->setToolTip(obs_module_text("MarketplaceWindow.Settings.EnableMakerTools.Tooltip")); - layout->addWidget(_makerCheckbox); + auto makerToolsTip = new QLabel(obs_module_text("MarketplaceWindow.Settings.EnableMakerTools.Tip"), this); + makerToolsTip->setStyleSheet(EWizardCheckBoxTipStyle); + + auto makerLayout = new QVBoxLayout(); + makerLayout->setContentsMargins(0, 0, 0, 0); + makerLayout->setSpacing(4); + makerLayout->addWidget(_makerCheckbox); + makerLayout->addWidget(makerToolsTip); + + layout->addLayout(makerLayout); connect(_makerCheckbox, &QCheckBox::stateChanged, [this](int state) { bool makerTools = state == Qt::Checked; @@ -566,31 +615,37 @@ ElgatoCloudConfig::ElgatoCloudConfig(QWidget *parent) : QDialog(parent) }); std::string restartTxt = elgatoCloud->MakerToolsOnStart() ? obs_module_text("MarketplaceWindow.Settings.MakerToolsRestartWarning.Disabled") : obs_module_text("MarketplaceWindow.Settings.MakerToolsRestartWarning.Enabled"); - _makerRestartMsg = new QLabel(restartTxt.c_str(), this); - _makerRestartMsg->setVisible(false); - _makerRestartMsg->setStyleSheet( - "QLabel {color: #FFFFFF; padding: 8px 16px 0px 16px; font-size: 12pt; font-style: italic; }"); - _makerRestartMsg->setAlignment(Qt::AlignCenter); + //_makerRestartMsg = new QLabel(restartTxt.c_str(), this); + //_makerRestartMsg->setVisible(false); + //_makerRestartMsg->setStyleSheet( + // "QLabel {color: #FFFFFF; padding: 8px 16px 0px 16px; font-size: 12pt; font-style: italic; }"); + //_makerRestartMsg->setAlignment(Qt::AlignCenter); + + std::string infoIconPath = imageBaseDir + "info-icon.svg"; + _makerRestartMsg = new InfoLabel(restartTxt.c_str(), infoIconPath.c_str(), this); + _makerRestartMsg->setVisible(makerTools != elgatoCloud->MakerToolsOnStart()); layout->addWidget(_makerRestartMsg); layout->addStretch(); - std::string version = "v"; - version += versionNoBuild() + releaseType() + " (build " + buildNumber() +")"; + std::string version = ""; + version += versionNoBuild() + releaseType() + " (" + buildNumber() +")"; auto versionLabel = new QLabel(version.c_str(), this); - layout->addWidget(versionLabel); + versionLabel->setStyleSheet(EWizardSmallLabel); + //layout->addWidget(versionLabel); auto buttons = new QHBoxLayout(); QPushButton *cancelButton = new QPushButton(this); cancelButton->setText( obs_module_text("General.CancelButton")); - cancelButton->setStyleSheet(EPushButtonCancelStyle); + cancelButton->setStyleSheet(EWizardQuietButtonStyle); QPushButton *saveButton = new QPushButton(this); saveButton->setText( obs_module_text("General.SaveButton")); - saveButton->setStyleSheet(EPushButtonStyle); + saveButton->setStyleSheet(EWizardButtonStyle); + buttons->addWidget(versionLabel); buttons->addStretch(); buttons->addWidget(cancelButton); buttons->addWidget(saveButton); @@ -608,7 +663,7 @@ ElgatoCloudConfig::ElgatoCloudConfig(QWidget *parent) : QDialog(parent) [this]() { close(); }); layout->addLayout(buttons); - setStyleSheet("background-color: #232323"); + setStyleSheet("background-color: #151515"); setLayout(layout); obs_data_release(config); } @@ -720,8 +775,8 @@ void ElgatoCloudConfig::DrawVideoPreview(void *data, uint32_t cx, uint32_t cy) return; uint32_t sourceCX = - std::max(obs_source_get_width(window->_videoCaptureSource), 1u); - uint32_t sourceCY = std::max( + (std::max)(obs_source_get_width(window->_videoCaptureSource), 1u); + uint32_t sourceCY = (std::max)( obs_source_get_height(window->_videoCaptureSource), 1u); int x, y; diff --git a/src/elgato-cloud-config.hpp b/src/elgato-cloud-config.hpp index f1294f4..19171c6 100644 --- a/src/elgato-cloud-config.hpp +++ b/src/elgato-cloud-config.hpp @@ -31,6 +31,7 @@ with this program. If not, see #include #include "qt-display.hpp" +#include "elgato-widgets.hpp" namespace elgatocloud { @@ -57,7 +58,7 @@ class DefaultAVWidget : public QWidget { obs_source_t *_videoCaptureSource = nullptr; OBSQTDisplay *_videoPreview = nullptr; QPushButton *_settingsButton = nullptr; - QLabel *_blank = nullptr; + CameraPlaceholder *_blank = nullptr; QComboBox *_videoSources = nullptr; std::vector _videoSourceIds; std::string _sourceName; @@ -138,7 +139,7 @@ class ElgatoCloudConfig : public QDialog { obs_volmeter_t *_volmeter = nullptr; OBSQTDisplay *_videoPreview = nullptr; QCheckBox *_makerCheckbox = nullptr; - QLabel* _makerRestartMsg = nullptr; + InfoLabel* _makerRestartMsg = nullptr; std::vector _toEnable; std::string _installDirectory; diff --git a/src/elgato-cloud-data.cpp b/src/elgato-cloud-data.cpp index ef54c35..3d1a269 100644 --- a/src/elgato-cloud-data.cpp +++ b/src/elgato-cloud-data.cpp @@ -30,11 +30,13 @@ with this program. If not, see #include #include #include +#include #include #include "elgato-cloud-window.hpp" #include "elgato-cloud-data.hpp" #include "elgato-update-modal.hpp" +#include "scene-collection-info.hpp" #include "platform.h" #include "util.h" #include "api.hpp" @@ -65,6 +67,7 @@ ElgatoCloud::ElgatoCloud(obs_module_t *m) //_translate = t; _securerand = QRandomGenerator::securelySeeded(); obs_frontend_add_event_callback(ElgatoCloud::FrontEndEventHandler, this); + obs_frontend_add_save_callback(ElgatoCloud::FrontEndSaveLoadHandler, this); _Initialize(); _Listen(); } @@ -72,6 +75,7 @@ ElgatoCloud::ElgatoCloud(obs_module_t *m) ElgatoCloud::~ElgatoCloud() { obs_frontend_remove_event_callback(ElgatoCloud::FrontEndEventHandler, this); + obs_frontend_remove_save_callback(ElgatoCloud::FrontEndSaveLoadHandler, this); obs_data_release(_config); } @@ -94,6 +98,47 @@ void ElgatoCloud::FrontEndEventHandler(enum obs_frontend_event event, void* data } } +void ElgatoCloud::FrontEndSaveLoadHandler(obs_data_t* save_data, bool saving, void* data) +{ + auto ec = static_cast(data); + if (!saving) { // We are loading + auto settings = obs_data_get_obj(save_data, "elgato_marketplace_connect"); + if (!settings) { // Not an elgato mp installed scene collection + return; + } + bool firstRun = obs_data_get_bool(settings, "first_run"); + auto thirdParty = obs_data_get_array(settings, "third_party"); + size_t tpSize = thirdParty ? obs_data_array_count(thirdParty) : 0; + if (firstRun && tpSize > 0) { + std::vector rows; + for (size_t i = 0; i < tpSize; i++) { + obs_data_t* item = obs_data_array_item(thirdParty, i); + std::string name = obs_data_get_string(item, "name"); + std::string url = obs_data_get_string(item, "url"); + rows.push_back({ name, url }); + obs_data_release(item); + } + + const auto mainWindow = static_cast(obs_frontend_get_main_window()); + SceneCollectionInfo* dialog = nullptr; + dialog = new SceneCollectionInfo(rows, mainWindow); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); + } + obs_data_release(settings); + if(thirdParty) + obs_data_array_release(thirdParty); + } else { + auto settings = obs_data_get_obj(save_data, "elgato_marketplace_connect"); + if (!settings) { // Not an elgato mp installed scene collection + return; + } + obs_data_set_bool(settings, "first_run", false); + obs_data_set_obj(save_data, "elgato_marketplace_connect", settings); + obs_data_release(settings); + } +} + obs_data_t *ElgatoCloud::GetConfig() { obs_data_addref(_config); diff --git a/src/elgato-cloud-data.hpp b/src/elgato-cloud-data.hpp index c3064fb..fa5ec20 100644 --- a/src/elgato-cloud-data.hpp +++ b/src/elgato-cloud-data.hpp @@ -74,6 +74,7 @@ class ElgatoCloud { ElgatoCloudWindow *window = nullptr; inline bool MakerToolsOnStart() const { return _makerToolsOnStart; } static void FrontEndEventHandler(enum obs_frontend_event event, void* data); + static void FrontEndSaveLoadHandler(obs_data_t* save_data, bool saving, void* data); private: void _Initialize(); diff --git a/src/elgato-cloud-window.cpp b/src/elgato-cloud-window.cpp index ce1dc10..27d616b 100644 --- a/src/elgato-cloud-window.cpp +++ b/src/elgato-cloud-window.cpp @@ -55,8 +55,8 @@ ElgatoCloudWindow *ElgatoCloudWindow::window = nullptr; Avatar::Avatar(QWidget *parent) : QWidget(parent) { update(); - setFixedWidth(40); - setFixedHeight(40); + setFixedWidth(32); + setFixedHeight(32); } void Avatar::update() @@ -74,22 +74,32 @@ void Avatar::paintEvent(QPaintEvent *e) paint.setRenderHint(QPainter::Antialiasing); paint.setPen(_bgColor.c_str()); paint.setBrush(QBrush(_bgColor.c_str(), Qt::SolidPattern)); - paint.drawEllipse(1, 1, 38, 38); + paint.drawEllipse(5, 5, 22, 22); QFont font = paint.font(); - font.setPixelSize(24); + font.setPixelSize(14); paint.setPen("#FFFFFF"); paint.setFont(font); - paint.drawText(QRect(1, 1, 38, 38), Qt::AlignCenter, + paint.drawText(QRect(5, 5, 22, 22), Qt::AlignCenter, _character.c_str()); paint.end(); + +} + +void Avatar::mousePressEvent(QMouseEvent* event) +{ + if (event->button() == Qt::LeftButton) { + emit clicked(); + } + + QWidget::mousePressEvent(event); // Pass event along if needed } AvatarImage::AvatarImage(QWidget* parent) : QWidget(parent) { auto api = MarketplaceApi::getInstance(); - setFixedWidth(40); - setFixedHeight(40); + setFixedWidth(32); + setFixedHeight(32); std::string imageBaseDir = GetDataPath(); imageBaseDir += "/images/"; @@ -101,7 +111,7 @@ AvatarImage::AvatarImage(QWidget* parent) : QWidget(parent) QPixmap avatarPixmap = _setupImage(imagePath); auto layout = new QVBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); + layout->setContentsMargins(4, 4, 4, 4); layout->setSpacing(0); _avatarImg = new QLabel(this); _avatarImg->setPixmap(avatarPixmap); @@ -127,10 +137,19 @@ void AvatarImage::update() _avatarImg->setPixmap(avatarPixmap); } +void AvatarImage::mousePressEvent(QMouseEvent* event) +{ + if (event->button() == Qt::LeftButton) { + emit clicked(); + } + + QWidget::mousePressEvent(event); // Pass event along if needed +} + QPixmap AvatarImage::_setupImage(std::string imagePath) { - int targetHeight = 40; - int cornerRadius = 20; + int targetHeight = 24; + int cornerRadius = 12; QPixmap img; if (imagePath != "") @@ -156,139 +175,99 @@ QPixmap AvatarImage::_setupImage(std::string imagePath) return target; } -DownloadButton::DownloadButton(QWidget *parent) : QWidget(parent) +UserMenu::UserMenu(QWidget* parent) : QWidget(parent) { - std::string imageBaseDir = GetDataPath(); - imageBaseDir += "/images/"; - - //setFixedWidth(48); - - QHBoxLayout *layout = new QHBoxLayout(this); - _stackedWidget = new QStackedWidget(this); - _stackedWidget->setStyleSheet("background-color: #151515"); - _stackedWidget->setFixedSize(24, 24); - _stackedWidget->setSizePolicy(QSizePolicy::Preferred, - QSizePolicy::Preferred); - - _downloadButton = new QPushButton(this); - _downloadButton->setToolTip( - obs_module_text("MarketplaceWindow.DownloadButton.Tooltip")); - std::string downloadIconPath = imageBaseDir + "download.svg"; - std::string downloadIconHoverPath = imageBaseDir + "download_hover.svg"; - std::string downloadIconDisabledPath = - imageBaseDir + "download_disabled.svg"; - QString buttonStyle = EIconHoverDisabledButtonStyle; - buttonStyle.replace("${img}", downloadIconPath.c_str()); - buttonStyle.replace("${hover-img}", downloadIconHoverPath.c_str()); - buttonStyle.replace("${disabled-img}", - downloadIconDisabledPath.c_str()); - _downloadButton->setFixedSize(24, 24); - _downloadButton->setStyleSheet(buttonStyle); - _downloadButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - //_downloadButton->setDisabled(true); - connect(_downloadButton, &QPushButton::clicked, [this]() { - _stackedWidget->setCurrentIndex(1); - emit downloadClicked(); - }); - _stackedWidget->addWidget(_downloadButton); + QSize widgetSize(32, 32); + auto api = MarketplaceApi::getInstance(); + _menu = new QMenu(this); - _downloadProgress = new DownloadProgress(this); - _downloadProgress->setValue(0.0); - _downloadProgress->setMinimum(0.0); - _downloadProgress->setMaximum(100.0); - _downloadProgress->setFixedHeight(24); - _downloadProgress->setFixedWidth(24); + // Disabled "Account" label + std::string name = api->firstName() + " " + api->lastName(); + _accountTitle = new QAction(name.c_str(), _menu); + _accountTitle->setEnabled(false); + _menu->addAction(_accountTitle); - _stackedWidget->addWidget(_downloadProgress); + // View Account (active) + QAction* viewAccount = new QAction("View Account", _menu); + _menu->addAction(viewAccount); - QWidget *spacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + // Separator + _menu->addSeparator(); - layout->addWidget(spacer); - layout->addWidget(_stackedWidget); -} + QAction* signOut = new QAction("Sign out", _menu); + _menu->addAction(signOut); -void DownloadButton::setDisabled(bool disabled) -{ - _downloadButton->setDisabled(disabled); -} - -void DownloadButton::setValue(double value) -{ - _downloadProgress->setValue(value); -} + auto layout = new QVBoxLayout(this); + setFixedSize(widgetSize); + layout->setContentsMargins(0, 0, 0, 0); + _plainAvatar = new Avatar(this); + _imgAvatar = new AvatarImage(this); + _stack = new QStackedWidget(this); + _stack->setFixedSize(widgetSize); + _stack->addWidget(_plainAvatar); + _stack->addWidget(_imgAvatar); + _stack->setCurrentIndex(elgatoCloud->loggedIn && api->avatarReady() ? 1 : 0); + + connect(api, &MarketplaceApi::UserDetailsUpdated, this, [this, api]() { + bool avatar = elgatoCloud->loggedIn && api->hasAvatar(); + _stack->setCurrentIndex(avatar ? 1 : 0); + if (elgatoCloud->loggedIn) { + std::string name = api->firstName() + " " + api->lastName(); + _accountTitle->setText(name.c_str()); + _plainAvatar->update(); + } + }); -void DownloadButton::resetDownload() -{ - _stackedWidget->setCurrentIndex(0); - _downloadProgress->setValue(0.0); -} + connect(api, &MarketplaceApi::AvatarDownloaded, this, [this, api]() { + _stack->setCurrentIndex(elgatoCloud->loggedIn && api->avatarReady() ? 1 : 0); + if (elgatoCloud->loggedIn) { + std::string name = api->firstName() + " " + api->lastName(); + _accountTitle->setText(name.c_str()); + _plainAvatar->update(); + _imgAvatar->update(); + } + }); -DownloadProgress::DownloadProgress(QWidget *parent) : QWidget(parent) -{ - _width = 20; - _height = 20; - _minimumValue = 0; - _maximumValue = 100; - _value = 0; - _progressWidth = 2; -} + connect(_plainAvatar, &Avatar::clicked, [this]() { + _showModalMenu(); + }); -DownloadProgress::~DownloadProgress() {} + connect(_imgAvatar, &AvatarImage::clicked, [this]() { + _showModalMenu(); + }); -void DownloadProgress::setValue(double value) -{ - _value = value; - update(); -} + connect(viewAccount, &QAction::triggered, [api]() { + api->OpenAccountInBrowser(); + }); -void DownloadProgress::setMinimum(double minimum) -{ - _minimumValue = minimum; - update(); -} + connect(signOut, &QAction::triggered, []() { + elgatoCloud->LogOut(); + }); -void DownloadProgress::setMaximum(double maximum) -{ - _maximumValue = maximum; - update(); + _menu->setStyleSheet(R"( + QMenu::item:disabled { + background-color: transparent; + })"); } -void DownloadProgress::paintEvent(QPaintEvent *e) +void UserMenu::_showModalMenu() { - int margin = _progressWidth / 2; - - int width = _width - _progressWidth; - int height = _height - _progressWidth; - - double value = 360.0 * (_value - _minimumValue) / - (_maximumValue - _minimumValue); - - QPainter paint; - paint.begin(this); - paint.setRenderHint(QPainter::Antialiasing); - - QRect rect(0, 0, _width, _height); - paint.setPen(Qt::NoPen); - paint.drawRect(rect); - - QPen pen; - pen.setColor(QColor(32, 76, 254)); - pen.setWidth(_progressWidth); - - paint.setPen(pen); - paint.drawArc(margin, margin, width, height, 90.0 * 16.0, - -value * 16.0); - - QPen bgPen; - bgPen.setColor(QColor(59, 59, 59)); - bgPen.setWidth(_progressWidth); - paint.setPen(bgPen); - float remaining = 360.0 - value; - paint.drawArc(margin, margin, width, height, 90.0 * 16.0, - remaining * 16.0); - - paint.end(); + // Transparent modal overlay to enforce modality + // Create a transparent non-modal QWidget to act as an overlay + QWidget* overlay = new QWidget(nullptr, Qt::Tool | Qt::FramelessWindowHint); + overlay->setAttribute(Qt::WA_TranslucentBackground); + overlay->setAttribute(Qt::WA_DeleteOnClose); + overlay->setWindowModality(Qt::ApplicationModal); // Soft application modality + overlay->resize(qApp->primaryScreen()->size()); + overlay->move(0, 0); + overlay->show(); + + // Position and show the menu + QPoint menuPos = this->mapToGlobal(QPoint(0, this->height())); + _menu->popup(menuPos); + + // Close the overlay when the menu is dismissed + connect(_menu, &QMenu::aboutToHide, overlay, &QWidget::close); } WindowToolBar::WindowToolBar(QWidget *parent) : QWidget(parent) @@ -296,87 +275,58 @@ WindowToolBar::WindowToolBar(QWidget *parent) : QWidget(parent) std::string imageBaseDir = GetDataPath(); imageBaseDir += "/images/"; - auto api = MarketplaceApi::getInstance(); + setFixedHeight(40); - QPalette pal = QPalette(); - pal.setColor(QPalette::Window, "#151515"); - setAutoFillBackground(true); - setPalette(pal); + auto api = MarketplaceApi::getInstance(); + setAttribute(Qt::WA_StyledBackground, true); + setStyleSheet("background-color: #000000;"); _layout = new QHBoxLayout(this); - _layout->setContentsMargins(16, 12, 16, 12); - _layout->setSpacing(16); + _layout->setContentsMargins(8, 8, 8, 0); + _layout->setSpacing(0); _logo = new QLabel(this); - std::string logoPath = imageBaseDir + "mp-logo-full.png"; + std::string logoPath = imageBaseDir + "marketplace-full-logo.svg"; QPixmap logoPixmap = QPixmap(logoPath.c_str()); _logo->setPixmap(logoPixmap); _layout->addWidget(_logo); _layout->addStretch(); - _settingsButton = new QPushButton(this); - _settingsButton->setToolTip(obs_module_text( - "MarketplaceWindow.OpenSettingsButton.Tooltip")); - std::string settingsIconPath = imageBaseDir + "settings.svg"; - std::string settingsIconHoverPath = imageBaseDir + "settings_hover.svg"; - QString settingsButtonStyle = EIconHoverButtonStyle; - settingsButtonStyle.replace("${img}", settingsIconPath.c_str()); - settingsButtonStyle.replace("${hover-img}", - settingsIconHoverPath.c_str()); - _settingsButton->setFixedSize(24, 24); - _settingsButton->setMaximumHeight(24); - _settingsButton->setStyleSheet(settingsButtonStyle); - _settingsButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - - connect(_settingsButton, &QPushButton::pressed, this, - [this]() { emit settingsClicked(); }); - _layout->addWidget(_settingsButton); - _storeButton = new QPushButton(this); _storeButton->setToolTip( obs_module_text("MarketplaceWindow.StoreButton.Tooltip")); - std::string storeIconPath = imageBaseDir + "marketplace-logo.svg"; - std::string storeIconHoverPath = - imageBaseDir + "marketplace-logo_hover.svg"; - QString buttonStyle = EIconHoverButtonStyle; + std::string storeIconPath = imageBaseDir + "button-marketplace-icon.svg"; + QString buttonStyle = EIconOnlyButtonStyle; buttonStyle.replace("${img}", storeIconPath.c_str()); - buttonStyle.replace("${hover-img}", storeIconHoverPath.c_str()); - _storeButton->setFixedSize(24, 24); - _storeButton->setMaximumHeight(24); + _storeButton->setFixedSize(32, 32); + _storeButton->setMaximumHeight(32); _storeButton->setStyleSheet(buttonStyle); _storeButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); connect(_storeButton, &QPushButton::pressed, this, [this, api]() { api->OpenStoreInBrowser(); - }); + }); _layout->addWidget(_storeButton); - _logInButton = new QPushButton(this); - _logInButton->setText( - obs_module_text("MarketplaceWindow.LoginButton.LogIn")); - _logInButton->setHidden(elgatoCloud->loggedIn); - _logInButton->setStyleSheet( - "QPushButton {font-size: 12pt; border-radius: 8px; padding: 8px; background-color: #232323; border: none; } " - "QPushButton:hover {background-color: #444444; }"); - _layout->addWidget(_logInButton); - connect(_logInButton, &QPushButton::clicked, this, - [this]() { elgatoCloud->StartLogin(); }); + _settingsButton = new QPushButton(this); + _settingsButton->setToolTip(obs_module_text( + "MarketplaceWindow.OpenSettingsButton.Tooltip")); + std::string settingsIconPath = imageBaseDir + "button-settings-icon.svg"; + QString settingsButtonStyle = EIconOnlyButtonStyle; + settingsButtonStyle.replace("${img}", settingsIconPath.c_str()); + _settingsButton->setFixedSize(32, 32); + _settingsButton->setMaximumHeight(32); + _settingsButton->setStyleSheet(settingsButtonStyle); + _settingsButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - _logOutButton = new QPushButton(this); - _logOutButton->setText( - obs_module_text("MarketplaceWindow.LoginButton.LogOut")); - _logOutButton->setHidden(!elgatoCloud->loggedIn); - _logOutButton->setStyleSheet( - "QPushButton {font-size: 12pt; border-radius: 8px; padding: 8px; background-color: #232323; border: none; } " - "QPushButton:hover {background-color: #444444; }"); - connect(_logOutButton, &QPushButton::clicked, this, - [this]() { elgatoCloud->LogOut(); }); + connect(_settingsButton, &QPushButton::pressed, this, + [this]() { emit settingsClicked(); }); + _layout->addWidget(_settingsButton); - _layout->addWidget(_logOutButton); - _avatar = new Avatar(this); - _avatarImage = new AvatarImage(this); - _layout->addWidget(_avatar); - _layout->addWidget(_avatarImage); + _userMenu = new UserMenu(this); + _userMenu->setHidden(!elgatoCloud->loggedIn); + + _layout->addWidget(_userMenu); connect(api, &MarketplaceApi::AvatarDownloaded, this, [this]() { updateState(); @@ -387,20 +337,13 @@ WindowToolBar::~WindowToolBar() {} void WindowToolBar::disableLogout(bool disabled) { - _logOutButton->setDisabled(disabled); +// _logOutButton->setDisabled(disabled); } void WindowToolBar::updateState() { auto api = MarketplaceApi::getInstance(); - _logInButton->setHidden(elgatoCloud->loggedIn); - _logOutButton->setHidden(!elgatoCloud->loggedIn); - _avatar->setHidden(!elgatoCloud->loggedIn || api->avatarReady()); - _avatarImage->setHidden(!elgatoCloud->loggedIn || !api->avatarReady()); - if (elgatoCloud->loggedIn) { - _avatar->update(); - _avatarImage->update(); - } + _userMenu->setHidden(!elgatoCloud->loggedIn); } Placeholder::Placeholder(QWidget *parent, std::string message) : QWidget(parent) @@ -446,12 +389,14 @@ size_t ProductGrid::loadProducts() return elgatoCloud->products.size(); } -void ProductGrid::disableDownload() +void ProductGrid::disableDownload(ElgatoProductItem* skip) { for (int i = 0; i < layout()->count(); ++i) { auto item = dynamic_cast( layout()->itemAt(i)->widget()); - item->disableDownload(); + if (item != skip) { + item->disableDownload(); + } } } @@ -464,18 +409,42 @@ void ProductGrid::enableDownload() } } +void ProductGrid::resetDownloads() +{ + for (int i = 0; i < layout()->count(); ++i) { + auto item = dynamic_cast( + layout()->itemAt(i)->widget()); + item->resetDownload(); + } +} + +void ProductGrid::closing() { + for (int i = 0; i < layout()->count(); ++i) { + auto item = dynamic_cast( + layout()->itemAt(i)->widget()); + item->closing(); + } +} + OwnedProducts::OwnedProducts(QWidget *parent) : QWidget(parent) { auto api = MarketplaceApi::getInstance(); + + std::string imageBaseDir = GetDataPath(); + imageBaseDir += "/images/"; + std::string iconPath = imageBaseDir + "your-library-icon.svg"; + _layout = new QHBoxLayout(this); _sideMenu = new QListWidget(this); - _sideMenu->addItem(obs_module_text("MarketplaceWindow.PurchasedTab")); - //_sideMenu->addItem("Installed (#)"); + QIcon icon(iconPath.c_str()); + auto yourLibrary = new QListWidgetItem(icon, obs_module_text("MarketplaceWindow.PurchasedTab")); + _sideMenu->setIconSize(QSize(20, 20)); + _sideMenu->addItem(yourLibrary); _sideMenu->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); - _sideMenu->setStyleSheet( - "QListWidget { border: none; font-size: 12pt; outline: none; } QListWidget::item { padding: 12px; border-radius: 8px; } QListWidget::item:selected { background-color: #444444} QListWidget::item:hover { background-color: #656565; }"); + _sideMenu->setStyleSheet(ELeftNavListStyle); _sideMenu->setCurrentRow(0); + _sideMenu->setFixedWidth(240); connect(_sideMenu, &QListWidget::itemPressed, this, [this](QListWidgetItem *item) { QString val = item->text(); @@ -490,12 +459,20 @@ OwnedProducts::OwnedProducts(QWidget *parent) : QWidget(parent) } }); + connect(api, &MarketplaceApi::UserDetailsUpdated, this, [this, api]() { + if (api->loggedIn()) { + refreshProducts(); + } + }); + _content = new QStackedWidget(this); _installed = new Placeholder(this, "Installed, not yet implemented..."); auto scroll = new QScrollArea(this); scroll->setWidgetResizable(true); - scroll->setStyleSheet("border: none;"); + //scroll->setStyleSheet("border: none;"); + scroll->setStyleSheet(ESlateContainerStyle); + _purchased = new ProductGrid(this); _purchased->setSizePolicy(QSizePolicy::Expanding, @@ -503,30 +480,29 @@ OwnedProducts::OwnedProducts(QWidget *parent) : QWidget(parent) refreshProducts(); auto noProducts = new QWidget(this); + noProducts->setStyleSheet(ESlateContainerStyle); auto npLayout = new QVBoxLayout(noProducts); npLayout->addStretch(); auto npTitle = new QLabel( obs_module_text("MarketplaceWindow.Purchased.NoPurchasesTitle"), noProducts); - npTitle->setStyleSheet("QLabel {font-size: 18pt;}"); + npTitle->setStyleSheet(EBlankSlateTitleStyle); npTitle->setAlignment(Qt::AlignCenter); npLayout->addWidget(npTitle); auto npSubTitle = new QLabel( obs_module_text( "MarketplaceWindow.Purchased.NoPurchasesSubtitle"), noProducts); - npSubTitle->setStyleSheet("QLabel {font-size: 13pt;}"); + npSubTitle->setStyleSheet(EBlankSlateSubTitleStyle); npSubTitle->setAlignment(Qt::AlignCenter); npLayout->addWidget(npSubTitle); - npLayout->setSpacing(16); + npLayout->setSpacing(8); auto hLayout = new QHBoxLayout(); auto mpButton = new QPushButton(this); mpButton->setText( obs_module_text("MarketplaceWindow.Purchased.OpenMarketplaceButton")); - mpButton->setStyleSheet( - "QPushButton {font-size: 12pt; border-radius: 8px; padding: 16px; background-color: #232323; border: none; } " - "QPushButton:hover {background-color: #444444; }"); + mpButton->setStyleSheet(EBlankSlateButtonStyle); connect(mpButton, &QPushButton::clicked, this, [this, api]() { api->OpenStoreInBrowser(); }); @@ -553,10 +529,22 @@ void OwnedProducts::refreshProducts() _numProducts = _purchased->loadProducts(); if (_numProducts == 0) { _content->setCurrentIndex(2); + } else { + _content->setCurrentIndex(0); } } } +void OwnedProducts::closing() +{ + _purchased->closing(); +} + +void OwnedProducts::resetDownloads() +{ + _purchased->resetDownloads(); +} + ElgatoCloudWindow::ElgatoCloudWindow(QWidget *parent) : QDialog(parent) //ui(new Ui_ElgatoCloudWindow) { @@ -579,10 +567,10 @@ ElgatoCloudWindow::~ElgatoCloudWindow() void ElgatoCloudWindow::initialize() { setWindowTitle(QString("Elgato Marketplace Connect")); - setFixedSize(1140, 600); + //setFixedSize(1140, 600); QPalette pal = QPalette(); - pal.setColor(QPalette::Window, "#151515"); + pal.setColor(QPalette::Window, "#000000"); setAutoFillBackground(true); setPalette(pal); @@ -596,12 +584,14 @@ void ElgatoCloudWindow::initialize() mainLayout->setContentsMargins(0, 0, 0, 0); _toolbar = new WindowToolBar(_mainWidget); + //_toolbar->setStyleSheet("background-color: #000000"); + connect(_toolbar, &WindowToolBar::settingsClicked, this, [this]() { _config = openConfigWindow(this); }); mainLayout->addWidget(_toolbar); _stackedContent = new QStackedWidget(_mainWidget); - _stackedContent->setStyleSheet("background-color: #151515"); + _stackedContent->setStyleSheet("background-color: #000000"); _stackedContent->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); @@ -632,6 +622,14 @@ void ElgatoCloudWindow::initialize() _layout->addWidget(_mainWidget); setLayout(_layout); + setFixedSize(978, 520); +} + +void ElgatoCloudWindow::resetDownloads() +{ + if (_ownedProducts) { + _ownedProducts->resetDownloads(); + } } void ElgatoCloudWindow::setLoading() @@ -640,6 +638,15 @@ void ElgatoCloudWindow::setLoading() _stackedContent->setCurrentIndex(3); } +void ElgatoCloudWindow::closeEvent(QCloseEvent* event) +{ + // TODO: Possibly add dialog to confirm if the user wants + // to close the window if an active download is in + // progress? + _ownedProducts->closing(); + event->accept(); +} + void ElgatoCloudWindow::on_logInButton_clicked() { elgatoCloud->StartLogin(); @@ -678,12 +685,295 @@ void ElgatoCloudWindow::setupOwnedProducts() _stackedContent->setCurrentIndex(0); } +ProgressThumbnail::ProgressThumbnail(float hoverOpacity, bool hoverDisabled, QWidget* parent) + : QLabel(parent), _hoverOpacity(hoverOpacity), _hoverDisabled(hoverDisabled), _opacity(1.0f) +{ + setAttribute(Qt::WA_TranslucentBackground); + setStyleSheet("background: transparent;"); + setMinimumSize(1, 1); +} + +void ProgressThumbnail::setCustomPixmap(const QPixmap& pixmap) +{ + setCustomPixmap(pixmap, size()); +} + +void ProgressThumbnail::setCustomPixmap(const QPixmap& pixmap, const QSize& size) +{ + _pixmap = pixmap; + _pixmapScaled = _pixmap.scaledToWidth(size.width(), Qt::SmoothTransformation); + QImage grayImage = _pixmapScaled.toImage().convertToFormat(QImage::Format_Grayscale8); + _pixmapScaledDisabled = QPixmap::fromImage(grayImage); + updateGeometry(); + update(); +} + +QSize ProgressThumbnail::sizeHint() const +{ + //auto size = _pixmapScaled.size(); + //return _pixmapScaled.size(); + if (_pixmap.isNull()) + return QLabel::sizeHint(); + + int labelWidth = width() > 0 ? width() : _pixmap.width(); + int scaledHeight = (labelWidth * _pixmap.height()) / _pixmap.width(); + return QSize(labelWidth, scaledHeight); +} + +void ProgressThumbnail::setProgress(float progress) +{ + // Clamp value between 0.0 and 1.0 + _progress = std::clamp(progress, 0.0f, 1.0f); + update(); +} + +void ProgressThumbnail::resizeEvent(QResizeEvent* event) +{ + QLabel::resizeEvent(event); + setCustomPixmap(_pixmap, event->size()); +} + +void ProgressThumbnail::setDisabled(bool disabled) +{ + _disabled = disabled; + update(); +} + +void ProgressThumbnail::setDownloading(bool downloading) +{ + _downloading = downloading; + if (!downloading) { + setProgress(1.0); + } + _opacity = 1.0f; + update(); +} + +void ProgressThumbnail::paintEvent(QPaintEvent* event) +{ + if (_pixmap.isNull()) return; + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + painter.setRenderHint(QPainter::SmoothPixmapTransform); + + // Rounded corners + const int radius = 8; + QPainterPath path; + path.addRoundedRect(rect(), radius, radius); + painter.setClipPath(path); + + QSize labelSize = size(); + float progress = _downloading ? _progress : 1.0f; + + int splitX = static_cast(labelSize.width() * progress); + int height = _pixmapScaled.height(); + int width = labelSize.width(); + float opacity = _downloading ? 1.0 : _opacity; + + painter.setOpacity(opacity); + if (!_disabled || _downloading) { + painter.drawPixmap(0, 0, splitX, height, _pixmapScaled, 0, 0, splitX, height); + } else { + painter.drawPixmap(0, 0, splitX, height, _pixmapScaledDisabled, 0, 0, splitX, height); + } + + QRect rightSide(splitX, 0, width - splitX, height); + QColor bgColor(42,42,42); + painter.fillRect(rightSide, bgColor); + + painter.setOpacity(1.0); + QColor borderColor(255, 255, 255, 64); + QPen pen(borderColor); + pen.setWidthF(2.0); + painter.setPen(pen); + painter.setBrush(Qt::NoBrush); + + painter.drawRoundedRect(rect(), radius, radius); + if (progress > 0.0f && progress < 1.0f) { + painter.drawLine(splitX, 0, splitX, height); + } +} + +void ProgressThumbnail::onHoverEnter(QHoverEvent* event) +{ + if (_hoverDisabled) { + return; + } + _opacity = _hoverOpacity; + update(); +} + +void ProgressThumbnail::onHoverLeave(QHoverEvent* event) +{ + if (_hoverDisabled) { + return; + } + _opacity = 1.0f; + update(); +} + +void ProgressThumbnail::disableHover() +{ + _hoverDisabled = true; + _opacity = 1.0f; + update(); +} + +void ProgressThumbnail::enableHover() +{ + _hoverDisabled = false; + _opacity = 0.5f; + update(); +} + +ProductThumbnail::ProductThumbnail(QWidget* parent, const QPixmap& pixmap) + : QWidget(parent) +{ + setAttribute(Qt::WA_Hover); + auto layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + _thumbnail = new ProgressThumbnail(0.1f, false, this); + _thumbnail->setCustomPixmap(pixmap); + _thumbnail->setProgress(1.0); + + _downloadButton = new QPushButton(this); + + _downloadButton->hide(); + connect(_downloadButton, &QPushButton::clicked, this, [this]() { + if (!_downloading) { + setDownloading(true); + emit downloadClicked(); + } else { + setDownloading(false); + emit cancelDownloadClicked(); + } + updateButton_(); + }); + + updateButton_(); + layout->addWidget(_thumbnail); +} + +void ProductThumbnail::updateButton_() +{ + std::string imageBaseDir = GetDataPath(); + imageBaseDir += "/images/"; + if (!_downloading) { + std::string iconPath = imageBaseDir + "button-download.svg"; + QPixmap iconPixmap(iconPath.c_str()); + QIcon downloadIcon(iconPixmap); + _downloadButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); + _downloadButton->setFixedHeight(32); + _downloadButton->setMinimumSize(QSize(0, 0)); + _downloadButton->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); + _downloadButton->setIcon(downloadIcon); + _downloadButton->setIconSize(iconPixmap.rect().size()); + _downloadButton->setStyleSheet(EBlankSlateButtonStyle); + _downloadButton->setText("Install"); + } else { + std::string stopIconPath = imageBaseDir + "button-stop-download.svg"; + QPixmap stopIconPixmap(stopIconPath.c_str()); + QIcon stopDownloadIcon(stopIconPixmap); + _downloadButton->setText(""); + _downloadButton->setMinimumSize(32, 32); + _downloadButton->setMaximumSize(32, 32); + _downloadButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + _downloadButton->setIcon(stopDownloadIcon); + _downloadButton->setIconSize(stopIconPixmap.rect().size()); + _downloadButton->setStyleSheet(EStopDownloadButtonStyle); + } + _downloadButton->adjustSize(); + _downloadButton->updateGeometry(); + QSize thumbSize = _thumbnail->size(); + QSize buttonSize = _downloadButton->size(); + int offsetX = (thumbSize.width() - buttonSize.width()) / 2; + int offsetY = (thumbSize.height() - buttonSize.height()) / 2; + _downloadButton->setGeometry(offsetX, offsetY, buttonSize.width(), buttonSize.height()); + layout()->activate(); +} + +void ProductThumbnail::setPixmap(const QPixmap& pixmap) +{ + _thumbnail->setCustomPixmap(pixmap); +} + +bool ProductThumbnail::event(QEvent* e) +{ + //auto buttonSize = _downloadButton->size(); + if (!_downloading && _disable) { + return QWidget::event(e); + } + QSize thumbSize; + QSize buttonSize; + int offsetX, offsetY; + switch (e->type()) { + case QEvent::HoverEnter: + thumbSize = _thumbnail->size(); + buttonSize = _downloadButton->size(); + offsetX = (thumbSize.width() - buttonSize.width()) / 2; + offsetY = (thumbSize.height() - buttonSize.height()) / 2; + if (!_downloading) { + _thumbnail->onHoverEnter(static_cast(e)); + } + _downloadButton->setGeometry(offsetX, offsetY, buttonSize.width(), buttonSize.height()); + _downloadButton->show(); + return true; + case QEvent::HoverLeave: + if (!_downloading) { + _thumbnail->onHoverLeave(static_cast(e)); + } + _downloadButton->hide(); + return true; + default: + break; + } + return QWidget::event(e); +} + +void ProductThumbnail::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); +} + +QSize ProductThumbnail::sizeHint() const +{ + return _thumbnail ? _thumbnail->sizeHint() : QWidget::sizeHint(); +} + +void ProductThumbnail::disable(bool disable) +{ + _disable = disable; + _thumbnail->setDisabled(disable); + update(); +} + +void ProductThumbnail::enable(bool enable) +{ + _disable = !enable; + _thumbnail->setDisabled(_disable); + update(); +} + +void ProductThumbnail::setDownloading(bool downloading) +{ + _thumbnail->setDownloading(downloading); + update(); + _downloading = downloading; +} + +ProductThumbnail::~ProductThumbnail() +{ + //if (_downloading) { + // emit cancelDownloadClicked(); + //} +} + ElgatoProductItem::ElgatoProductItem(QWidget *parent, ElgatoProduct *product) : QWidget(parent), _product(product) { - setFixedWidth(270); - //setStyleSheet("QWidget { border: 1px solid #FFF; }"); + setFixedWidth(220); std::string imageBaseDir = GetDataPath(); imageBaseDir += "/images/"; @@ -695,59 +985,67 @@ ElgatoProductItem::ElgatoProductItem(QWidget *parent, ElgatoProduct *product) product->SetProductItem(this); - QPixmap previewImage = _setupImage(imagePath); + QPixmap previewImage(imagePath.c_str()); auto titleLayout = new QVBoxLayout(); titleLayout->setSpacing(0); - std::string name = _product->name.size() > 24 - ? _product->name.substr(0, 24) + "..." + std::string name = _product->name.size() > 25 + ? _product->name.substr(0, 23) + "..." : _product->name; QLabel *label = new QLabel(name.c_str(), this); label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); label->setMinimumWidth(150); - label->setStyleSheet("QLabel {font-size: 11pt;}"); + label->setStyleSheet("QLabel {font-size: 13px; font-weight: 600; }"); titleLayout->addWidget(label); auto subTitle = new QLabel("Scene Collection", this); - subTitle->setStyleSheet("QLabel {font-size: 10pt; color: #7E7E7E; }"); + subTitle->setStyleSheet("QLabel {font-size: 12px; color: rgba(255, 255, 255, 0.67); }"); titleLayout->addWidget(subTitle); - _downloadButton = new DownloadButton(this); + _labelImg = new ProductThumbnail(this, previewImage); + _labelImg->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - connect(_downloadButton, &DownloadButton::downloadClicked, [this]() { - auto p = dynamic_cast(parentWidget()); - p->disableDownload(); + connect(_labelImg, &ProductThumbnail::downloadClicked, [this]() { + auto p = dynamic_cast(parentWidget()); + p->disableDownload(this); bool success = _product->DownloadProduct(); if (!success) { p->enableDownload(); resetDownload(); } + else { + p->enableDownload(); + resetDownload(); + } + }); + + connect(_labelImg, &ProductThumbnail::cancelDownloadClicked, [this]() { + _product->StopProductDownload(); + auto p = dynamic_cast(parentWidget()); + p->enableDownload(); + resetDownload(); }); - auto labelLayout = new QHBoxLayout(); - labelLayout->addLayout(titleLayout); - QWidget *spacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - //spacer->setStyleSheet("QWidget { border: 1px solid #FFFFFF; }"); - labelLayout->addWidget(spacer); - labelLayout->addWidget(_downloadButton); - - _labelImg = new QLabel(this); - _labelImg->setPixmap(previewImage); - _labelImg->setSizePolicy(QSizePolicy::Preferred, - QSizePolicy::Preferred); layout->addWidget(_labelImg); - layout->addLayout(labelLayout); + layout->addLayout(titleLayout); setLayout(layout); } -ElgatoProductItem::~ElgatoProductItem() {} +ElgatoProductItem::~ElgatoProductItem() { + _product->StopProductDownload(); +} + +void ElgatoProductItem::closing() { + _product->StopProductDownload(); +} void ElgatoProductItem::resetDownload() { //_labelDownload->setCurrentIndex(0); - _downloadButton->resetDownload(); + setDisabled(false); + _labelImg->disable(false); + _labelImg->setDownloading(false); auto pw = dynamic_cast(parentWidget()); pw->enableDownload(); } @@ -755,13 +1053,16 @@ void ElgatoProductItem::resetDownload() void ElgatoProductItem::disableDownload() { //_downloadButton->setVisible(false); - _downloadButton->setDisabled(true); + setDisabled(true); + //_labelImg->setDisabled(true); + _labelImg->disable(true); } void ElgatoProductItem::enableDownload() { //_downloadButton->setVisible(true); - _downloadButton->setDisabled(false); + setDisabled(false); + _labelImg->disable(false); } void ElgatoProductItem::updateImage() @@ -773,6 +1074,7 @@ void ElgatoProductItem::updateImage() : imageBaseDir + "image-loading.svg"; QPixmap image = _setupImage(imagePath); _labelImg->setPixmap(image); + _labelImg->update(); } QPixmap ElgatoProductItem::_setupImage(std::string imagePath) @@ -801,30 +1103,42 @@ QPixmap ElgatoProductItem::_setupImage(std::string imagePath) painter.setClipPath(path); painter.drawPixmap(0, 0, img.scaledToHeight(targetHeight)); + QColor borderColor(255, 255, 255, 32); + QPen pen(borderColor); + pen.setWidth(1); + painter.setPen(pen); + painter.setBrush(Qt::NoBrush); + + QRectF rect(0, 0, targetWidth, targetHeight); + painter.drawRoundedRect(rect, 8, 8); return target; } void ElgatoProductItem::UpdateDownload(bool downloading, int progress) { if (downloading) { - _downloadButton->setValue(progress); + _labelImg->updateDownloadProgress((float)progress/100.0f); } } LoginNeeded::LoginNeeded(QWidget *parent) : QWidget(parent) { auto layout = new QVBoxLayout(this); + auto container = new QWidget(this); + container->setStyleSheet(ESlateContainerStyle); + setContentsMargins(0, 0, 0, 0); + auto containerLayout = new QVBoxLayout(container); auto login = new QLabel(this); login->setText(obs_module_text("MarketplaceWindow.LoginNeeded.Title")); - login->setStyleSheet("QLabel {font-size: 18pt; }"); + login->setStyleSheet(EBlankSlateTitleStyle); login->setAlignment(Qt::AlignCenter); auto subHLayout = new QHBoxLayout(); auto loginSub = new QLabel(this); loginSub->setText( obs_module_text("MarketplaceWindow.LoginNeeded.Subtitle")); - loginSub->setStyleSheet("QLabel {font-size: 13pt; }"); + loginSub->setStyleSheet(EBlankSlateSubTitleStyle); loginSub->setFixedWidth(480); loginSub->setWordWrap(true); loginSub->setAlignment(Qt::AlignCenter); @@ -836,37 +1150,44 @@ LoginNeeded::LoginNeeded(QWidget *parent) : QWidget(parent) auto loginButton = new QPushButton(this); loginButton->setText( obs_module_text("MarketplaceWindow.LoginButton.LogIn")); - loginButton->setStyleSheet( - "QPushButton {font-size: 12pt; border-radius: 8px; padding: 16px; background-color: #232323; border: none; } " - "QPushButton:hover {background-color: #444444; }"); + QString loginButtonStyle = EBlankSlateButtonStyle; + loginButton->setStyleSheet(loginButtonStyle); connect(loginButton, &QPushButton::clicked, this, [this]() { elgatoCloud->StartLogin(); }); hLayout->addStretch(); hLayout->addWidget(loginButton); hLayout->addStretch(); - layout->addStretch(); - layout->addWidget(login); - layout->addLayout(subHLayout); - layout->addLayout(hLayout); - layout->addStretch(); - layout->setSpacing(16); + containerLayout->addStretch(); + containerLayout->addWidget(login); + containerLayout->addLayout(subHLayout); + containerLayout->addLayout(hLayout); + containerLayout->addStretch(); + containerLayout->setSpacing(8); + + layout->addWidget(container); } LoggingIn::LoggingIn(QWidget* parent) : QWidget(parent) { auto layout = new QVBoxLayout(this); + auto container = new QWidget(this); + container->setStyleSheet(ESlateContainerStyle); + setContentsMargins(0, 0, 0, 0); + auto containerLayout = new QVBoxLayout(container); + + auto spinner = new SmallSpinner(this); auto login = new QLabel(this); login->setText(obs_module_text("MarketplaceWindow.LoggingIn.Title")); - login->setStyleSheet("QLabel {font-size: 18pt; }"); + login->setStyleSheet(EBlankSlateTitleStyle); login->setAlignment(Qt::AlignCenter); auto subHLayout = new QHBoxLayout(); auto loginSub = new QLabel(this); loginSub->setText( obs_module_text("MarketplaceWindow.LoggingIn.Subtitle")); - loginSub->setStyleSheet("QLabel {font-size: 13pt; }"); + loginSub->setStyleSheet(EBlankSlateSubTitleStyle); loginSub->setFixedWidth(480); loginSub->setWordWrap(true); loginSub->setAlignment(Qt::AlignCenter); @@ -878,20 +1199,21 @@ LoggingIn::LoggingIn(QWidget* parent) : QWidget(parent) auto loginButton = new QPushButton(this); loginButton->setText( obs_module_text("MarketplaceWindow.LoggingIn.TryAgain")); - loginButton->setStyleSheet( - "QPushButton {font-size: 12pt; border-radius: 8px; padding: 16px; background-color: #232323; border: none; } " - "QPushButton:hover {background-color: #444444; }"); + loginButton->setStyleSheet(EBlankSlateQuietButtonStyle); connect(loginButton, &QPushButton::clicked, this, [this]() { elgatoCloud->StartLogin(); }); hLayout->addStretch(); hLayout->addWidget(loginButton); hLayout->addStretch(); - layout->addStretch(); - layout->addWidget(login); - layout->addLayout(subHLayout); - layout->addLayout(hLayout); - layout->addStretch(); - layout->setSpacing(16); + containerLayout->addStretch(); + containerLayout->addWidget(spinner); + containerLayout->addWidget(login); + containerLayout->addLayout(subHLayout); + containerLayout->addLayout(hLayout); + containerLayout->addStretch(); + containerLayout->setSpacing(8); + + layout->addWidget(container); } LoginError::LoginError(QWidget *parent) : QWidget(parent) @@ -971,16 +1293,27 @@ LoadingWidget::LoadingWidget(QWidget *parent) : QWidget(parent) auto layout = new QVBoxLayout(this); auto loading = new QLabel(this); loading->setText(obs_module_text("MarketplaceWindow.Loading.Title")); - loading->setStyleSheet("QLabel {font-size: 18pt;}"); + loading->setStyleSheet(EBlankSlateTitleStyle); + loading->setFixedWidth(360); + loading->setWordWrap(true); loading->setAlignment(Qt::AlignCenter); + auto loadingLayout = new QHBoxLayout(); + loadingLayout->addStretch(); + loadingLayout->addWidget(loading); + loadingLayout->addStretch(); //auto spinner = new ProgressSpinner(this); - auto spinner = new SpinnerPanel(this, "", "", true); - spinner->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + //auto spinner = new SpinnerPanel(this, "", "", true); + auto spinner = new SmallSpinner(this); + spinner->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + auto spinnerLayout = new QHBoxLayout(); layout->addStretch(); - layout->addWidget(spinner); - layout->addWidget(loading); + spinnerLayout->addStretch(); + spinnerLayout->addWidget(spinner); + spinnerLayout->addStretch(); + layout->addLayout(spinnerLayout); + layout->addLayout(loadingLayout); layout->addStretch(); layout->setSpacing(16); } diff --git a/src/elgato-cloud-window.hpp b/src/elgato-cloud-window.hpp index d602ce3..0b13469 100644 --- a/src/elgato-cloud-window.hpp +++ b/src/elgato-cloud-window.hpp @@ -18,6 +18,9 @@ with this program. If not, see #pragma once +#include +#include +#include #include #include #include @@ -28,6 +31,7 @@ with this program. If not, see #include #include #include +#include #include #include "elgato-product.hpp" #include "elgato-cloud-config.hpp" @@ -42,8 +46,12 @@ class Avatar : public QWidget { Avatar(QWidget *parent); void update(); +signals: + void clicked(); + protected: void paintEvent(QPaintEvent *e); + void mousePressEvent(QMouseEvent* event) override; private: std::string _character; @@ -55,50 +63,96 @@ class AvatarImage : public QWidget { public: AvatarImage(QWidget* parent); void update(); + +signals: + void clicked(); + +protected: + void mousePressEvent(QMouseEvent* event) override; + private: QPixmap _setupImage(std::string imagePath); QLabel* _avatarImg; }; -class DownloadProgress : public QWidget { +class UserMenu : public QWidget { Q_OBJECT - public: - DownloadProgress(QWidget *parent); - ~DownloadProgress(); - void setMinimum(double minimum); - void setMaximum(double maximum); - void setValue(double value); + UserMenu(QWidget* parent); -protected: - void paintEvent(QPaintEvent *e) override; +signals: + void viewAccountClicked(); + void signOutClicked(); private: - int _width; - int _height; - int _progressWidth; - double _minimumValue; - double _maximumValue; - double _value; + QMenu* _menu; + QStackedWidget* _stack; + Avatar* _plainAvatar; + AvatarImage* _imgAvatar; + QAction* _accountTitle; + +private slots: + void _showModalMenu(); + }; -class DownloadButton : public QWidget { +class ProgressThumbnail : public QLabel { Q_OBJECT - public: - DownloadButton(QWidget *parent); - void setDisabled(bool disabled); - void setValue(double value); - void resetDownload(); + ProgressThumbnail(float hoverOpacity, bool hoverDisabled, QWidget* parent = nullptr); + void setCustomPixmap(const QPixmap& pixmap); + void setCustomPixmap(const QPixmap& pixmap, const QSize& size); + void setProgress(float progress); + void setDownloading(bool downloading); + void onHoverEnter(QHoverEvent* event); + void onHoverLeave(QHoverEvent* event); + void disableHover(); + void enableHover(); + QSize sizeHint() const override; + virtual void setDisabled(bool disabled); +protected: + void paintEvent(QPaintEvent* event) override; + virtual void resizeEvent(QResizeEvent* event); private: - DownloadProgress *_downloadProgress; - QPushButton *_downloadButton; - QStackedWidget *_stackedWidget; + QPixmap _pixmap, _pixmapScaled, _pixmapScaledDisabled; + float _progress = 0.5f; + float _hoverOpacity; + float _opacity; + bool _hoverDisabled; + bool _disabled = false; + bool _downloading = false; +}; + +class ProductThumbnail : public QWidget { + Q_OBJECT + +public: + ProductThumbnail(QWidget* parent, const QPixmap& pixmap); + ~ProductThumbnail(); + void setPixmap(const QPixmap& pixmap); + inline void updateDownloadProgress(float progress) { _thumbnail->setProgress(progress); } + void disable(bool disable); + void enable(bool enable); + void setDownloading(bool downloading); + QSize sizeHint() const override; signals: void downloadClicked(); + void cancelDownloadClicked(); + +protected: + bool event(QEvent* e); + void resizeEvent(QResizeEvent* event) override; + +private: + void updateButton_(); + ProgressThumbnail* _thumbnail; + QPushButton* _downloadButton; + QPushButton* _stopDownloadButton; + bool _disable = false; + bool _downloading = false; }; class ElgatoProductItem : public QWidget { @@ -112,11 +166,11 @@ class ElgatoProductItem : public QWidget { void resetDownload(); void disableDownload(); void enableDownload(); + void closing(); private: - ElgatoProduct *_product; - DownloadButton *_downloadButton; - QLabel *_labelImg; + ElgatoProduct* _product; + ProductThumbnail* _labelImg; QPixmap _setupImage(std::string imagePath); }; @@ -138,6 +192,7 @@ class WindowToolBar : public QWidget { QPushButton *_logOutButton; Avatar *_avatar; AvatarImage* _avatarImage; + UserMenu* _userMenu; signals: void settingsClicked(); @@ -149,8 +204,10 @@ class ProductGrid : public QWidget { ProductGrid(QWidget *parent); ~ProductGrid(); size_t loadProducts(); - void disableDownload(); + void disableDownload(ElgatoProductItem* skip = nullptr); void enableDownload(); + void resetDownloads(); + void closing(); private: FlowLayout *_layout; @@ -173,6 +230,8 @@ class OwnedProducts : public QWidget { ~OwnedProducts(); void refreshProducts(); + void closing(); + void resetDownloads(); private: QHBoxLayout *_layout = nullptr; @@ -229,6 +288,7 @@ class ElgatoCloudWindow : public QDialog { void setLoggedIn(); void setLoading(); void setupOwnedProducts(); + void resetDownloads(); static ElgatoCloudWindow *window; @@ -236,6 +296,9 @@ public slots: void on_logInButton_clicked(); void on_logOutButton_clicked(); +protected: + void closeEvent(QCloseEvent* event) override; + private: QVBoxLayout *_layout = nullptr; QWidget *_mainWidget = nullptr; diff --git a/src/elgato-product.cpp b/src/elgato-product.cpp index d9b415b..60a50aa 100644 --- a/src/elgato-product.cpp +++ b/src/elgato-product.cpp @@ -35,12 +35,12 @@ with this program. If not, see #include #include -#include "downloader.h" #include "elgato-cloud-data.hpp" #include "elgato-cloud-window.hpp" #include "util.h" #include "setup-wizard.hpp" + namespace elgatocloud { ElgatoProduct::ElgatoProduct(nlohmann::json &productData) : _fileSize(0) @@ -77,10 +77,10 @@ ElgatoProduct::ElgatoProduct(std::string collectionName) : name(collectionName), thumbnailUrl(""), variantId(""), - _fileSize(0) + _fileSize(0), + downloading_(false) { - thumbnailPath = obs_get_module_data_path(obs_current_module()); - thumbnailPath += "/images/thumbnail-holder.png"; + thumbnailPath = ""; _thumbnailReady = true; } @@ -120,10 +120,23 @@ bool ElgatoProduct::DownloadProduct() os_mkdirs(savePath.c_str()); std::shared_ptr dl = Downloader::getInstance(""); - dl->Enqueue(url, savePath, ElgatoProduct::DownloadProgress, nullptr, this); + auto download = dl->Enqueue(url, savePath, ElgatoProduct::DownloadProgress, nullptr, this); + downloadId_ = download.id; + downloading_ = true; return true; } +void ElgatoProduct::StopProductDownload() +{ + if (downloading_) { + std::shared_ptr dl = Downloader::getInstance(""); + auto download = dl->Lookup(downloadId_); + download.id = downloadId_; + download.Stop(); + downloading_ = false; + } +} + void ElgatoProduct::_downloadThumbnail() { std::string savePath = QDir::homePath().toStdString(); @@ -178,6 +191,7 @@ void ElgatoProduct::Install(std::string filename_utf8, void *data, bool fromDownload) { auto ep = static_cast(data); + ep->downloading_ = false; const auto mainWindow = static_cast(obs_frontend_get_main_window()); if (ep->_productItem) { diff --git a/src/elgato-product.hpp b/src/elgato-product.hpp index 01ccd43..234406b 100644 --- a/src/elgato-product.hpp +++ b/src/elgato-product.hpp @@ -22,6 +22,8 @@ with this program. If not, see #include +#include "downloader.h" + namespace elgatocloud { class ElgatoProductItem; @@ -45,6 +47,7 @@ class ElgatoProduct { inline ~ElgatoProduct() {}; inline bool ready() { return _thumbnailReady; } bool DownloadProduct(); + void StopProductDownload(); static void DownloadProgress(void *ptr, bool finished, bool downloading, uint64_t fileSize, uint64_t chunkSize, uint64_t downloaded); @@ -60,6 +63,8 @@ class ElgatoProduct { bool _thumbnailReady; size_t _fileSize; ElgatoProductItem *_productItem = nullptr; + size_t downloadId_; + bool downloading_; }; } // namespace elgatocloud diff --git a/src/elgato-styles.hpp b/src/elgato-styles.hpp index b81f201..44cdba3 100644 --- a/src/elgato-styles.hpp +++ b/src/elgato-styles.hpp @@ -20,214 +20,513 @@ with this program. If not, see #include namespace elgatocloud { -// Constant strings for different element styles -// Cant use QT style sheets in a plugin reliably, -// so instead we encode the styles as constant -// strings. - -inline const QString EListStyle = "QListWidget {" - "border: none;" - "background: #151515;" - "border-radius: 8px;" - "}" - "QListWidget::item {" - "border: none;" - "padding: 8px;" - "background-color: #232323;" - "border-radius: 8px;" - "}" - "QListWidget::item:selected {" - "border: none;" - "}"; - -inline const QString EMissingPluginsStyle = "QListWidget {" - "border: none;" - "background: #151515;" - "border-radius: 8px;" - "}" - "QListWidget::item {" - "border: none;" - "padding: 0px;" - "background-color: #232323;" - "border-radius: 8px;" - "}" - "QListWidget::item:selected {" - "border: none;" - "}"; - -inline const QString EPushButtonStyle = "QPushButton {" - "font-size: 12pt;" - "padding: 8px 36px 8px 36px;" - "background-color: #204cfe;" - "border-radius: 8px;" - "border: none;" - "}" - "QPushButton:hover {" - "background-color: #193ed4;" - "}" - "QPushButton:disabled {" - "background-color: #1c1c1c;" - "border: none;" - "}"; - -inline const QString EPushButtonDarkStyle = "QPushButton {" - "font-size: 12pt;" - "padding: 8px 36px 8px 36px;" - "background-color: #3b3b3b;" - "border-radius: 8px;" - "border: none;" - "}" - "QPushButton:hover {" - "background-color: #193ed4;" - "}" - "QPushButton:disabled {" - "background-color: #1c1c1c;" - "border: none;" - "}"; - -inline const QString EPushButtonCancelStyle = "QPushButton {" - "font-size: 12pt;" - "padding: 8px 36px 8px 36px;" - "background-color: #414141;" - "border-radius: 8px;" - "border: none;" - "}" - "QPushButton:hover {" - "background-color: #193ed4;" - "}" - "QPushButton:disabled {" - "background-color: #1c1c1c;" - "border: none;" - "}"; - -inline const QString ELineEditStyle = "QLineEdit {" - "background-color: #151515;" - "border: none;" - "padding: 12px;" - "font-size: 11pt;" - "border-radius: 8px;" - "}"; - -inline const QString ETitleStyle = - "QLabel{ font-size: 18pt; font-weight: bold; }"; - -inline const QString EChecklistStyleTemplate = - "QListWidget {" - "border: none;" - "background: #151515;" - "border-radius: 8px;" - "}" - "QListWidget::item {" - "border: none;" - "padding: 8px;" - "background-color: #232323;" - "border-radius: 8px;" - "}" - "QListWidget::item:selected {" - "border: none;" - "}" - "QListWidget::indicator {" - "width: 20px;" - "height: 20px;" - "background: none;" - "}" - "QListWidget::indicator:checked {" - "background-image: url('${checked-img}')" - "}" - "QListWidget::indicator:unchecked {" - "background-image: url('${unchecked-img}')" - "}"; - -inline const QString ECheckBoxStyle = - "QCheckBox {" - "margin-left: 16px;" - "margin-top: 8px;" - "font-size: 12pt;" - "}" - "QCheckBox::indicator {" - "width: 20px;" - "height: 20px;" - "}" - "QCheckBox::indicator:checked {" - "image: url('${checked-img}')" - "}" - "QCheckBox::indicator:unchecked {" - "image: url('${unchecked-img}')" - "}"; - -inline const QString EComboBoxStyle = "QComboBox {" - "background-color: #151515;" - "border: none;" - "padding: 12px;" - "font-size: 11pt;" - "border-radius: 8px;" - "}"; - -inline const QString EComboBoxStyleLight = "QComboBox {" - "background-color: #FFFFFF;" - "color: #444444;" - "border: none;" - "padding: 12px;" - "font-size: 11pt;" - "border-radius: 8px;" - "}"; - -inline const QString EIconButtonStyle = "QPushButton {" - "background: transparent;" - "border: none;" - "padding-left: 4px;" - "padding-right: 4px;" - "}"; - -inline const QString EIconHoverButtonStyle = - "QPushButton {" - "background: transparent;" - "background-repeat: no-repeat;" - "border: none;" - "width: 24px;" - "height: 24px;" - "padding: 0px;" - "margin: 0px;" - "border-image: url('${img}');" - "}" - - "QPushButton:hover {" - "border-image: url('${hover-img}');" - "}"; - -inline const QString EInlineIconHoverButtonStyle = - "QPushButton {" - "background: transparent;" - "background-repeat: no-repeat;" - "border: none;" - //"width: 24px;" - //"height: 24px;" - "padding: 0px;" - "margin: 0px 16px 0px 0px;" - "border-image: url('${img}');" - "}" - - "QPushButton:hover {" - "border-image: url('${hover-img}');" - "}"; - -inline const QString EIconHoverDisabledButtonStyle = - "QPushButton {" - "background: transparent;" - "background-repeat: no-repeat;" - "border: none;" - "width: 24px;" - "height: 24px;" - "padding: 0px;" - "margin: 0px;" - "border-image: url('${img}');" - "}" - - "QPushButton:hover {" - "border-image: url('${hover-img}');" - "}" - - "QPushButton:disabled {" - "border-image: url('${disabled-img}');" - "}"; + // Constant strings for different element styles + // Cant use QT style sheets in a plugin reliably, + // so instead we encode the styles as constant + // strings. + + inline const QString EListStyle = "QListWidget {" + "border: none;" + "background: #151515;" + "border-radius: 8px;" + "}" + "QListWidget::item {" + "border: none;" + "padding: 8px;" + "background-color: #232323;" + "border-radius: 8px;" + "}" + "QListWidget::item:selected {" + "border: none;" + "}"; + + inline const QString EMissingPluginsStyle = "QListWidget {" + "border: none;" + "background: #151515;" + "border-radius: 8px;" + "}" + "QListWidget::item {" + "border: none;" + "padding: 0px;" + "font-size: 16px;" + "background-color: #232323;" + "border-radius: 8px;" + "}" + "QListWidget::item:selected {" + "border: none;" + "}"; + + inline const QString EPushButtonStyle = "QPushButton {" + "font-size: 12pt;" + "padding: 8px 36px 8px 36px;" + "background-color: #204cfe;" + "border-radius: 8px;" + "border: none;" + "}" + "QPushButton:hover {" + "background-color: #193ed4;" + "}" + "QPushButton:disabled {" + "background-color: #1c1c1c;" + "border: none;" + "}"; + + inline const QString EPushButtonDarkStyle = "QPushButton {" + "font-size: 12pt;" + "padding: 8px 36px 8px 36px;" + "background-color: #3b3b3b;" + "border-radius: 8px;" + "border: none;" + "}" + "QPushButton:hover {" + "background-color: #193ed4;" + "}" + "QPushButton:disabled {" + "background-color: #1c1c1c;" + "border: none;" + "}"; + + inline const QString EPushButtonCancelStyle = "QPushButton {" + "font-size: 12pt;" + "padding: 8px 36px 8px 36px;" + "background-color: #414141;" + "border-radius: 8px;" + "border: none;" + "}" + "QPushButton:hover {" + "background-color: #193ed4;" + "}" + "QPushButton:disabled {" + "background-color: #1c1c1c;" + "border: none;" + "}"; + + inline const QString ELineEditStyle = "QLineEdit {" + "background-color: #151515;" + "border: none;" + "padding: 12px;" + "font-size: 11pt;" + "border-radius: 8px;" + "}"; + + inline const QString ETitleStyle = + "QLabel{ font-size: 18pt; font-weight: bold; }"; + + inline const QString EChecklistStyleTemplate = + "QListWidget {" + "border: none;" + "background: #151515;" + "border-radius: 8px;" + "}" + "QListWidget::item {" + "border: none;" + "padding: 8px;" + "background-color: #232323;" + "border-radius: 8px;" + "}" + "QListWidget::item:selected {" + "border: none;" + "}" + "QListWidget::indicator {" + "width: 20px;" + "height: 20px;" + "background: none;" + "}" + "QListWidget::indicator:checked {" + "background-image: url('${checked-img}')" + "}" + "QListWidget::indicator:unchecked {" + "background-image: url('${unchecked-img}')" + "}"; + + inline const QString ECheckBoxStyle = + "QCheckBox {" + "margin-left: 16px;" + "margin-top: 8px;" + "font-size: 12pt;" + "}" + "QCheckBox::indicator {" + "width: 20px;" + "height: 20px;" + "}" + "QCheckBox::indicator:checked {" + "image: url('${checked-img}')" + "}" + "QCheckBox::indicator:unchecked {" + "image: url('${unchecked-img}')" + "}"; + + inline const QString EComboBoxStyle = "QComboBox {" + "background-color: #151515;" + "border: none;" + "padding: 12px;" + "font-size: 11pt;" + "border-radius: 8px;" + "}"; + + inline const QString EComboBoxStyleLight = "QComboBox {" + "background-color: #FFFFFF;" + "color: #444444;" + "border: none;" + "padding: 12px;" + "font-size: 11pt;" + "border-radius: 8px;" + "}"; + + inline const QString EIconButtonStyle = "QPushButton {" + "background: transparent;" + "border: none;" + "padding-left: 4px;" + "padding-right: 4px;" + "}"; + + inline const QString EIconHoverButtonStyle = + "QPushButton {" + "background: transparent;" + "background-repeat: no-repeat;" + "border: none;" + "width: 24px;" + "height: 24px;" + "padding: 0px;" + "margin: 0px;" + "border-image: url('${img}');" + "}" + + "QPushButton:hover {" + "border-image: url('${hover-img}');" + "}"; + + inline const QString EInlineIconHoverButtonStyle = + "QPushButton {" + "background: transparent;" + "background-repeat: no-repeat;" + "border: none;" + //"width: 24px;" + //"height: 24px;" + "padding: 0px;" + "margin: 0px 16px 0px 0px;" + "border-image: url('${img}');" + "}" + + "QPushButton:hover {" + "border-image: url('${hover-img}');" + "}"; + + inline const QString EIconHoverDisabledButtonStyle = + "QPushButton {" + "background: transparent;" + "background-repeat: no-repeat;" + "border: none;" + "width: 24px;" + "height: 24px;" + "padding: 0px;" + "margin: 0px;" + "border-image: url('${img}');" + "}" + + "QPushButton:hover {" + "border-image: url('${hover-img}');" + "}" + + "QPushButton:disabled {" + "border-image: url('${disabled-img}');" + "}"; + + inline const QString EIconOnlyButtonStyle = + "QPushButton {" + "background: transparent;" + "background-repeat: no-repeat;" + "border-radius: 8px;" + "border: none;" + "width: 32px;" + "height: 32px;" + "margin: 0px;" + "border-image: url('${img}');" + "}" + "QPushButton:hover {" + "background: rgba(255, 255, 255, 0.2);" + "}" + "QPushButton:pressed {" + "background: rgba(255, 255, 255, 0.3);" + "}"; + + inline const QString EBlankSlateButtonStyle = + "QPushButton {" + "background: #204CFE;" + "border-radius: 8px;" + "border: none;" + "color: #FFFFFF;" + "font-size: 14px;" + "height: 32px;" + "padding-left: 8px;" + "padding-right: 8px;" + "}" + "QPushButton:hover {" + "background: #193ED4;" + "}" + "QPushButton:pressed {" + "background: #1231AC;" + "}"; + + inline const QString EBlankSlateQuietButtonStyle = + "QPushButton {" + "background: rgba(255, 255, 255, 0.1);" + "border-radius: 8px;" + "border: none;" + "color: #FFFFFF;" + "font-size: 14px;" + "height: 32px;" + "padding-left: 8px;" + "padding-right: 8px;" + "}" + "QPushButton:hover {" + "background: rgba(255, 255, 255, 0.2);" + "}" + "QPushButton:pressed {" + "background: rgba(255, 255, 255, 0.3);" + "}"; + + inline const QString EStopDownloadButtonStyle = + "QPushButton {" + "background: #2f2f2f;" + "border-radius: 8px;" + "border: none;" + "color: #FFFFFF;" + "font-size: 14px;" + "height: 32px;" + "padding-left: 8px;" + "padding-right: 8px;" + "}" + "QPushButton:hover {" + "background: #494949;" + "}" + "QPushButton:pressed {" + "background: #636363;" + "}"; + + inline const QString EBlankSlateTitleStyle = + "QLabel {" + "color: #FFFFFF;" + "font-weight: bold;" + "font-size: 16px;" + "}"; + + inline const QString EBlankSlateSubTitleStyle = + "QLabel {" + "color: rgba(255, 255, 255, 0.67);" + "font-size: 14px;" + "}"; + + inline const QString ESlateContainerStyle = + "QWidget{" + "background-color: #151515;" + "border-radius: 16px;" + "border: none;" + "}"; + + inline const QString ELeftNavListStyle = + "QListWidget {" + "border: none;" + "font-size: 14px;" + "outline: none;" + "padding-top: 8px;" + "}" + "QListWidget::item {" + "line-height: 20px;" + "font-size: 14px;" + "padding: 4px 8px;" + "border-radius: 8px;" + "background-color: #151515" + "}" + "QListWidget::item:selected {" + "background-color: #1f1f1f;" + "color: white;" + "}" + "QListWidget::item:hover {" + "background-color: #1f1f1f;" + "}"; + + inline const QString EStepperLabelPrior = + "QLabel {" + "color: #FFFFFF;" + "font-size: 14px;" + "font-weight: 400;" + "};"; + + inline const QString EStepperLabelCurrent = + "QLabel {" + "color: #FFFFFF;" + "font-size: 14px;" + "font-weight: 500;" + "};"; + + inline const QString EStepperLabelFuture = + "QLabel {" + "color: rgba(255, 255, 255, 0.67);" + "font-size: 14px;" + "font-weight: 500;" + "};"; + + inline const QString EWizardWindow = + "{" + "background-color: #151515;" + "}"; + + inline const QString EWizardStepTitle = + "QLabel {" + "color: #FFFFFF;" + "font-size: 16px;" + "font-weight: bold;" + "}"; + + inline const QString EWizardStepSubTitle = + "QLabel {" + "color: rgba(255, 255, 255, 0.67);" + "font-size: 14px;" + "font-weight: 400;" + "}"; + + inline const QString EWizardFieldLabel = + "QLabel {" + "color: #FFFFFF;" + "font-size: 14px;" + "font-weight: 400;" + "}"; + + inline const QString EWizardFieldLabelQuiet = + "QLabel {" + "color: rgba(255, 255, 255, 0.67);" + "font-size: 14px;" + "font-weight: 400;" + "}"; + + inline const QString EWizardTextField = + "QLineEdit {" + "border: 1px solid rgba(255, 255, 255, 0.12);" + "border-radius: 8px;" + "}"; + + inline const QString EWizardComboBoxStyle = "QComboBox {" + "background-color: #232323;" + "border: none;" + "padding: 6px 12px 6px 12px;" + "font-size: 14px;" + "border-radius: 8px;" + "}"; + + inline const QString EWizardButtonStyle = + "QPushButton {" + "background: #204CFE;" + "border-radius: 8px;" + "border: none;" + "color: #FFFFFF;" + "font-size: 14px;" + "height: 32px;" + "padding-left: 8px;" + "padding-right: 8px;" + "}" + "QPushButton:hover {" + "background: #193ED4;" + "}" + "QPushButton:pressed {" + "background: #1231AC;" + "}"; + + inline const QString EWizardQuietButtonStyle = + "QPushButton {" + "background: rgba(255, 255, 255, 0.1);" + "border-radius: 8px;" + "border: none;" + "color: #FFFFFF;" + "font-size: 14px;" + "height: 32px;" + "padding-left: 8px;" + "padding-right: 8px;" + "}" + "QPushButton:hover {" + "background: rgba(255, 255, 255, 0.2);" + "}" + "QPushButton:pressed {" + "background: rgba(255, 255, 255, 0.3);" + "}"; + + inline const QString EWizardSmallLabel = + "QLabel {" + "color: rgba(255, 255, 255, 0.67);" + "font-size: 12px;" + "font-weight: 400;" + "}"; + + inline const QString EWizardCheckBoxStyle = + "QCheckBox {" + "font-size: 14px;" + "}" + "QCheckBox::indicator {" + "width: 16px;" + "height: 16px;" + "}" + "QCheckBox::indicator:checked {" + "image: url('${checked-img}');" + "}" + "QCheckBox::indicator:unchecked {" + "image: url('${unchecked-img}');" + "}"; + + inline const QString EWizardChecklistStyle = + "QListWidget {" + "border: none;" + "background: #151515;" + "border-radius: 8px;" + "font-size: 14px;" + "}" + "QListWidget::item {" + "border: none;" + "padding: 8px;" + "background-color: #232323;" + "border-radius: 8px;" + "}" + "QListWidget::item:selected {" + "border: none;" + "}" + "QListWidget::indicator {" + "width: 16px;" + "height: 16px;" + "}" + "QListWidget::indicator:checked {" + "background-image: url('${checked-img}');" + "}" + "QListWidget::indicator:unchecked {" + "background-image: url('${unchecked-img}');" + "}"; + + inline const QString EWizardCheckBoxTipStyle = + "QLabel {" + "font-size: 14px;" + "color: rgba(255, 255, 255, 0.67);" + "font-weight: 400;" + "margin-left: 20px;" + "}"; + + inline const QString EWizardIconOnlyButtonStyle = + "QPushButton {" + "background: transparent;" + "background-repeat: no-repeat;" + "border-radius: 8px;" + "border: none;" + "width: 32px;" + "height: 32px;" + "margin: 0px;" + "border-image: url('${img}');" + "background: #232323;" + "}" + "QPushButton:hover {" + "background: rgba(255, 255, 255, 0.2);" + "}" + "QPushButton:pressed {" + "background: rgba(255, 255, 255, 0.3);" + "}" + "QPushButton:disabled {" + "background: none;" + "border-image: url('${img-disabled}');" + "}"; } // namespace elgatocloud diff --git a/src/elgato-widgets.cpp b/src/elgato-widgets.cpp index 957718e..9b1c3b4 100644 --- a/src/elgato-widgets.cpp +++ b/src/elgato-widgets.cpp @@ -24,15 +24,29 @@ with this program. If not, see #include #include #include +#include +#include +#include #include #include +#include #include "elgato-widgets.hpp" #include "elgato-styles.hpp" #include "obs-utils.hpp" +#include "util.h" namespace elgatocloud { +QHBoxLayout* centeredWidgetLayout(QWidget* widget) +{ + auto layout = new QHBoxLayout(); + layout->addStretch(); + layout->addWidget(widget); + layout->addStretch(); + return layout; +} + VideoCaptureSourceSelector::VideoCaptureSourceSelector(QWidget *parent, std::string sourceLabel, std::string sourceName, @@ -42,52 +56,51 @@ VideoCaptureSourceSelector::VideoCaptureSourceSelector(QWidget *parent, _noneSelected(true), _deactivated(false) { - std::string imageBaseDir = - obs_get_module_data_path(obs_current_module()); - imageBaseDir += "/images/"; + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + std::string imageBaseDir = getImagesPath(); - setFixedWidth(258); + //std::string cameraIconSmPath = imageBaseDir + "camera-icon-sm.png"; + //QPixmap cameraIcon(cameraIconSmPath.c_str()); + std::string cameraIconPath = imageBaseDir + "icon-camera.svg"; _loading = true; auto layout = new QVBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 16); - auto videoDeviceLabel = new QLabel(this); + layout->setContentsMargins(0, 12, 0, 0); + layout->setSpacing(4); + + auto label = new IconLabel(cameraIconPath, sourceLabel, this); - videoDeviceLabel->setText(sourceLabel.c_str()); - videoDeviceLabel->setStyleSheet("QLabel { font-size: 12pt; }"); _videoSources = new QComboBox(this); - _videoSources->setStyleSheet(EComboBoxStyle); - - _videoPreview = new OBSQTDisplay(this); - _videoPreview->setFixedHeight(144); - _videoPreview->hide(); - - _blank = new QLabel(this); - _blank->setText(obs_module_text( - "MarketplaceWindow.Settings.DefaultVideoDevice.NoneSelected")); - _blank->setAlignment(Qt::AlignCenter); - _blank->setFixedHeight(144); - - auto videoSettings = new QHBoxLayout(this); - videoSettings->addWidget(_videoSources); - - layout->addWidget(videoDeviceLabel); - layout->addLayout(videoSettings); - layout->addWidget(_videoPreview); - layout->addWidget(_blank); - auto spacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); - layout->addWidget(spacer); + _videoSources->setStyleSheet(EWizardComboBoxStyle); + + _videoPreview = new OBSQTDisplay(); + + _stack = new QStackedWidget(this); + //_stack->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + _videoPreviewWidget = new VideoPreviewWidget(_videoPreview, 8, this); + _videoPreviewWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + + auto placeholder = new CameraPlaceholder(8, this); + std::string cameraPlaceholderIconPath = imageBaseDir + "camera-placeholder-icon.svg"; + placeholder->setIcon(cameraPlaceholderIconPath.c_str()); + _stack->addWidget(placeholder); + _stack->addWidget(_videoPreviewWidget); + + layout->addWidget(label); + layout->addWidget(_stack); + layout->addWidget(_videoSources); + + + layout->addStretch(); _setupTempSource(videoData); if (_videoSources->currentIndex() > 0) { this->_noneSelected = false; - this->_videoPreview->show(); - this->_blank->hide(); + _stack->setCurrentIndex(1); } else { this->_noneSelected = true; - this->_videoPreview->hide(); - this->_blank->show(); + _stack->setCurrentIndex(0); } connect(_videoSources, &QComboBox::currentIndexChanged, this, @@ -160,6 +173,14 @@ void VideoCaptureSourceSelector::_setupTempSource(obs_data_t *videoData) //obs_data_release(videoSettings); } +void VideoCaptureSourceSelector::resizeEvent(QResizeEvent* event) +{ + int newWidth = width(); + int newHeight = static_cast(newWidth * 9.0 / 16.0); + _stack->setFixedSize(QSize(newWidth, newHeight)); + update(); +} + void VideoCaptureSourceSelector::_changeSource(obs_data_t *vSettings) { if (vSettings != nullptr) { @@ -175,12 +196,10 @@ void VideoCaptureSourceSelector::_changeSource(obs_data_t *vSettings) vId, "elgato-cloud-video-config", vSettings); this->_noneSelected = false; - this->_videoPreview->show(); - this->_blank->hide(); + _stack->setCurrentIndex(1); } else { this->_noneSelected = true; - this->_videoPreview->hide(); - this->_blank->show(); + _stack->setCurrentIndex(0); if (_videoCaptureSource) { obs_source_t *tmp = _videoCaptureSource; _videoCaptureSource = nullptr; @@ -260,14 +279,61 @@ void VideoCaptureSourceSelector::EnableTempSource() calldata_free(&cd); } -ProgressSpinner::ProgressSpinner(QWidget *parent) : QWidget(parent) +VideoPreviewWidget::VideoPreviewWidget(OBSQTDisplay* videoPreview, int radius, QWidget* parent) + : QWidget(parent), _videoPreview(videoPreview), _radius(radius) +{ + _videoPreview->setParent(this); + _videoPreview->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + _videoPreview->show(); + setAttribute(Qt::WA_TransparentForMouseEvents); // Optional: pass events through if wrapper is decorative +} + +void VideoPreviewWidget::resizeEvent(QResizeEvent* event) +{ + //QWidget::resizeEvent(event); + QSize newSize = event->size(); + int newWidth = newSize.width(); + int newHeight = static_cast(newWidth * 9.0 / 16.0); + resize(newWidth, newHeight); + setFixedHeight(newHeight); + + if (_videoPreview) { + _videoPreview->setGeometry(QRect(0, 0, newWidth, newHeight)); + } + update(); // Trigger repaint for the border +} + +void VideoPreviewWidget::applyRoundedMask() +{ + QPainterPath path; + path.addRoundedRect(_videoPreview->rect(), _radius, _radius); + _videoPreview->setMask(QRegion(path.toFillPolygon().toPolygon())); +} + +void VideoPreviewWidget::paintEvent(QPaintEvent* event) +{ + QWidget::paintEvent(event); +} + +QSize VideoPreviewWidget::sizeHint() const +{ + // Return the preferred size of the widget + //QRectF rect = _videoPreview->rect(); + int w = width(); + int h = static_cast(w * 9.0 / 16.0); + return QSize(w, h); +} + + +ProgressSpinner::ProgressSpinner( + QWidget *parent, int width, int height, + int progressWidth, QColor fgColor, QColor bgColor) +: QWidget(parent), _width(width), _height(height), _progressWidth(progressWidth), + _fgColor(fgColor), _bgColor(bgColor), _blue(true) { - _width = 120; - _height = 120; _minimumValue = 0; _maximumValue = 100; _value = 0; - _progressWidth = 8; QPropertyAnimation *animBlue = new QPropertyAnimation(this, "valueBlue", this); @@ -324,8 +390,8 @@ void ProgressSpinner::paintEvent(QPaintEvent *e) paint.drawRect(rect); QPen pen; - QColor fg = _blue ? QColor(32, 76, 254) : QColor(200, 200, 200); - QColor bg = _blue ? QColor(200, 200, 200) : QColor(32, 76, 254); + QColor fg = _blue ? _fgColor : _bgColor; + QColor bg = _blue ? _bgColor : _fgColor; pen.setColor(fg); pen.setWidth(_progressWidth); @@ -350,8 +416,7 @@ SpinnerPanel::SpinnerPanel(QWidget *parent, std::string title, { QVBoxLayout *vLayout = new QVBoxLayout(); QHBoxLayout *hLayout = new QHBoxLayout(); - - auto spinner = new ProgressSpinner(this); + auto spinner = new ProgressSpinner(this, 124, 124, 8, QColor(32, 76, 254), QColor(200, 200, 200)); spinner->setFixedHeight(124); spinner->setFixedWidth(124); @@ -364,4 +429,404 @@ SpinnerPanel::SpinnerPanel(QWidget *parent, std::string title, setLayout(vLayout); } +SmallSpinner::SmallSpinner(QWidget* parent) + : QWidget(parent) +{ + QVBoxLayout* vLayout = new QVBoxLayout(); + QHBoxLayout* hLayout = new QHBoxLayout(); + auto spinner = new ProgressSpinner(this, 24, 24, 2, QColor(255, 255, 255), QColor(255, 255, 255, 25)); + spinner->setFixedHeight(24); + spinner->setFixedWidth(24); + + hLayout->addStretch(); + hLayout->addWidget(spinner); + hLayout->addStretch(); + vLayout->addLayout(hLayout); + setLayout(vLayout); +} + +StepperStep::StepperStep(std::string text, bool firstStep, QWidget* parent) + : QWidget(parent), _firstStep(firstStep), _status(FUTURE_STEP) +{ + std::string imageBaseDir = + obs_get_module_data_path(obs_current_module()); + imageBaseDir += "/images/"; + std::string currentMarkerPath = imageBaseDir + "stepper-marker-current.svg"; + std::string priorMarkerPath = imageBaseDir + "stepper-marker-prior.svg"; + std::string futureMarkerPath = imageBaseDir + "stepper-marker-future.svg"; + _currentMarker = QPixmap(currentMarkerPath.c_str()); + _priorMarker = QPixmap(priorMarkerPath.c_str()); + _futureMarker = QPixmap(futureMarkerPath.c_str()); + + auto layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(8); + if (!_firstStep) { + auto separatorLayout = new QHBoxLayout(); + separatorLayout->setContentsMargins(0, 0, 0, 0); + separatorLayout->setSpacing(0); + std::string activeIconPath = imageBaseDir + "stepper-separator-active.svg"; + std::string inactiveIconPath = imageBaseDir + "stepper-separator-inactive.svg"; + _activeSeparator = QPixmap(activeIconPath.c_str()); + _inactiveSeparator = QPixmap(inactiveIconPath.c_str()); + _separator = new QLabel(this); + _separator->setFixedSize(16, 16); + _separator->setPixmap(_inactiveSeparator); + separatorLayout->addWidget(_separator); + separatorLayout->addStretch(); + layout->addLayout(separatorLayout); + } else { + _separator = nullptr; + } + + auto labelLayout = new QHBoxLayout(); + labelLayout->setContentsMargins(0, 0, 0, 0); + labelLayout->setSpacing(8); + _marker = new QLabel(this); + _marker->setPixmap(_priorMarker); + //_marker->setFixedSize(16, 16); + _label = new QLabel(text.c_str(), this); + _label->setStyleSheet("QLabel { font-size: 14px; }"); + labelLayout->addWidget(_marker); + labelLayout->addWidget(_label); + labelLayout->addStretch(); + layout->addLayout(labelLayout); +} + +void StepperStep::setStatus(StepperStepStatus status) +{ + _status = status; + _update(); +} + +void StepperStep::_update() +{ + switch (_status) { + case PRIOR_STEP: + _marker->setPixmap(_priorMarker); + if (!_firstStep) { + _separator->setPixmap(_activeSeparator); + } + _label->setStyleSheet(EStepperLabelPrior); + break; + case FUTURE_STEP: + _marker->setPixmap(_futureMarker); + if (!_firstStep) { + _separator->setPixmap(_inactiveSeparator); + } + _label->setStyleSheet(EStepperLabelFuture); + break; + case CURRENT_STEP: + _marker->setPixmap(_currentMarker); + if (!_firstStep) { + _separator->setPixmap(_activeSeparator); + } + _label->setStyleSheet(EStepperLabelCurrent); + break; + } + update(); +} + +Stepper::Stepper(std::vector stepLabels, QWidget* parent) + : QWidget(parent) +{ + auto layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(8); + + int i = 0; + for (auto label : stepLabels) { + auto* step = new StepperStep(label, i == 0, this); + layout->addWidget(step); + _steps.append(step); + i++; + } + + _currentStep = 2; + _update(); +} + +void Stepper::setStep(int step) +{ + _currentStep = step; + _update(); +} + +void Stepper::incrementStep() +{ + _currentStep++; + _update(); +} + +void Stepper::decrementStep() +{ + if(_currentStep > 0) + _currentStep--; + _update(); +} + +void Stepper::_update() +{ + int i = 0; + for (auto step : _steps) { + StepperStepStatus status = + i == _currentStep ? CURRENT_STEP : + i < _currentStep ? PRIOR_STEP : + FUTURE_STEP; + step->setStatus(status); + i++; + } +} + +RoundedImageLabel::RoundedImageLabel(int cornerRadius, QWidget* parent) + : QLabel(parent), _cornerRadius(cornerRadius) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + setMinimumHeight(40); +} + +void RoundedImageLabel::setImage(const QPixmap& pixmap) +{ + originalPixmap = pixmap; + updateScaledPixmap(); + update(); +} + +void RoundedImageLabel::resizeEvent(QResizeEvent* event) +{ + QLabel::resizeEvent(event); + updateScaledPixmap(); +} + +void RoundedImageLabel::updateScaledPixmap() +{ + if (originalPixmap.isNull() || parentWidget() == nullptr) + return; + + int targetWidth = width(); + int newHeight = (targetWidth * originalPixmap.height()) / originalPixmap.width(); + QPixmap scaled = originalPixmap.scaled(targetWidth, newHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation); + + // Create rounded mask + QPixmap rounded(scaled.size()); + rounded.fill(Qt::transparent); + + QPainter painter(&rounded); + painter.setRenderHint(QPainter::Antialiasing); + + QPainterPath path; + path.addRoundedRect(scaled.rect(), _cornerRadius, _cornerRadius); + painter.setClipPath(path); + painter.drawPixmap(0, 0, scaled); + + // Draw 1px semi-transparent white border around the rounded rect + QColor strokeColor(255, 255, 255, 64); // 50% white + QPen pen(strokeColor, 1); + painter.setClipping(false); // Stop clipping so we can draw the stroke + painter.setPen(pen); + painter.drawPath(path); + + scaledPixmapWithRoundedCorners = rounded; + setFixedHeight(rounded.height()); +} + +void RoundedImageLabel::paintEvent(QPaintEvent* event) +{ + QLabel::paintEvent(event); + + if (!scaledPixmapWithRoundedCorners.isNull()) + { + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + int x = (width() - scaledPixmapWithRoundedCorners.width()) / 2; + painter.drawPixmap(x, 0, scaledPixmapWithRoundedCorners); + } +} + +CameraPlaceholder::CameraPlaceholder(int cornerRadius, QWidget* parent) + : QWidget(parent), _cornerRadius(cornerRadius), _svgRenderer(nullptr) { + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); +} + +void CameraPlaceholder::setIcon(const QString& svgFilePath) { + if (_svgRenderer) { + delete _svgRenderer; + _svgRenderer = nullptr; + } + + QFileInfo check(svgFilePath); + if (check.exists() && check.suffix().toLower() == "svg") { + _svgRenderer = new QSvgRenderer(svgFilePath, this); + + // Optional: get default size from SVG + _iconSize = _svgRenderer->defaultSize(); + if (_iconSize.isEmpty()) { + _iconSize = QSize(48, 48); // fallback size + } + + update(); // trigger repaint + } +} + +void CameraPlaceholder::resizeEvent(QResizeEvent* event) { + int w = event->size().width(); + int h = static_cast(w * 9.0 / 16.0); + resize(w, h); + QWidget::resizeEvent(event); + updateGeometry(); +} + +void CameraPlaceholder::paintEvent(QPaintEvent* event) { + Q_UNUSED(event); + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + QRectF rect = this->rect(); + + QPainterPath path; + path.addRoundedRect(rect, _cornerRadius, _cornerRadius); + painter.setClipPath(path); + + // Draw background + painter.fillRect(rect, QColor(35, 35, 35)); + + // Draw SVG in the center + if (_svgRenderer) { + QSizeF size = _iconSize; + QPointF topLeft((rect.width() - size.width()) / 2.0, (rect.height() - size.height()) / 2.0); + QRectF iconRect(topLeft, size); + _svgRenderer->render(&painter, iconRect); + } +} + +QSize CameraPlaceholder::sizeHint() const +{ + // Return the preferred size of the widget + QRectF rect = this->rect(); + int w = rect.width(); + int h = static_cast(w * 9.0 / 16.0); + return QSize(w, h); +} + +InfoLabel::InfoLabel(const QString& text, const QString& svgPath, QWidget* parent) + : QWidget(parent) { + + _iconLabel = new QLabel; + _textLabel = new QLabel; + + _iconLabel->setFixedSize(20, 20); + _iconLabel->setScaledContents(true); + _iconLabel->setAlignment(Qt::AlignCenter); + _iconLabel->setStyleSheet("QLabel {background: transparent;}"); + + _textLabel->setText(text); + _textLabel->setStyleSheet("QLabel {background: transparent; color: white; font-size: 14px;}"); + + QHBoxLayout* layout = new QHBoxLayout(this); + layout->setContentsMargins(16, 8, 16, 8); + layout->setSpacing(8); + layout->addWidget(_iconLabel); + layout->addWidget(_textLabel); + layout->addStretch(); + + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); + setMinimumHeight(30); + setAttribute(Qt::WA_StyledBackground); + + if (!svgPath.isEmpty()) { + setIconFromSvg(svgPath); + } +} + +void InfoLabel::setText(const QString& text) { + _textLabel->setText(text); +} + +void InfoLabel::setIconFromSvg(const QString& svgPath) { + QPixmap pixmap = _renderSvgToPixmap(svgPath, QSize(24, 24)); + _iconLabel->setPixmap(pixmap); +} + +QPixmap InfoLabel::_renderSvgToPixmap(const QString& svgPath, const QSize& size) { + QSvgRenderer renderer(svgPath); + + QImage image(size * devicePixelRatioF(), QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(devicePixelRatioF()); + image.fill(Qt::transparent); + + QPainter painter(&image); + painter.setRenderHint(QPainter::Antialiasing); + renderer.render(&painter, QRectF(QPointF(0, 0), QSizeF(size))); + + return QPixmap::fromImage(image); +} + +void InfoLabel::paintEvent(QPaintEvent* event) { + Q_UNUSED(event); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + + QPainterPath path; + path.addRoundedRect(rect(), 8, 8); + + painter.fillPath(path, QColor("#061965")); // Blue background +} + +StreamPackageHeader::StreamPackageHeader(QWidget* parent, std::string name, + std::string thumbnailPath = "") + : QWidget(parent) +{ + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setSpacing(16); + layout->setContentsMargins(0, 0, 0, 0); + QLabel* nameLabel = new QLabel(this); + nameLabel->setText(name.c_str()); + nameLabel->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Preferred); + nameLabel->setStyleSheet(EWizardStepTitle); + nameLabel->setWordWrap(true); + layout->addWidget(nameLabel); + if (thumbnailPath != "") { + auto thumbnail = new RoundedImageLabel(8, this); + QPixmap image(thumbnailPath.c_str()); + thumbnail->setImage(image); + layout->addWidget(thumbnail); + } else { + auto placeholder = new CameraPlaceholder(8, this); + std::string imageBaseDir = GetDataPath(); + imageBaseDir += "/images/"; + std::string imgPath = imageBaseDir + "icon-scene-collection.svg"; + placeholder->setIcon(imgPath.c_str()); + layout->addWidget(placeholder); + } +} + +IconLabel::IconLabel(const std::string& svgPath, const std::string& text, QWidget* parent) + : QWidget(parent) +{ + QHBoxLayout* layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(6); + + QLabel* iconLabel = new QLabel(this); + QSize iconSize(20, 20); + QPixmap pixmap(iconSize); + pixmap.fill(Qt::transparent); + + QSvgRenderer renderer(QString(svgPath.c_str())); + QPainter painter(&pixmap); + renderer.render(&painter); + + iconLabel->setPixmap(pixmap); + iconLabel->setFixedSize(iconSize); + + QLabel* textLabel = new QLabel(text.c_str(), this); + textLabel->setStyleSheet(EWizardFieldLabelQuiet); + + layout->addWidget(iconLabel); + layout->addWidget(textLabel); +} + + } // namespace elgatocloud diff --git a/src/elgato-widgets.hpp b/src/elgato-widgets.hpp index 269f09b..420d142 100644 --- a/src/elgato-widgets.hpp +++ b/src/elgato-widgets.hpp @@ -24,11 +24,19 @@ with this program. If not, see #include #include #include +#include +#include +#include +#include #include "qt-display.hpp" namespace elgatocloud { +QHBoxLayout* centeredWidgetLayout(QWidget* widget); + +class VideoPreviewWidget; + class VideoCaptureSourceSelector : public QWidget { Q_OBJECT @@ -44,9 +52,14 @@ class VideoCaptureSourceSelector : public QWidget { void DisableTempSource(); void EnableTempSource(); +protected: + void resizeEvent(QResizeEvent* event) override; + private: obs_source_t *_videoCaptureSource = nullptr; + VideoPreviewWidget* _videoPreviewWidget = nullptr; OBSQTDisplay *_videoPreview = nullptr; + QStackedWidget* _stack = nullptr; QLabel *_blank = nullptr; QComboBox *_videoSources = nullptr; std::vector _videoSourceIds; @@ -58,12 +71,31 @@ class VideoCaptureSourceSelector : public QWidget { bool _deactivated; }; +class VideoPreviewWidget : public QWidget { + Q_OBJECT + +public: + VideoPreviewWidget(OBSQTDisplay* videoPreview, int radius, QWidget* parent); + +protected: + void resizeEvent(QResizeEvent* event) override; + void paintEvent(QPaintEvent* event) override; + QSize sizeHint() const override; + +private: + OBSQTDisplay* _videoPreview; + int _radius; + + void applyRoundedMask(); +}; + class ProgressSpinner : public QWidget { Q_OBJECT Q_PROPERTY(double valueBlue WRITE setValueBlue READ getValue) Q_PROPERTY(double valueGrey WRITE setValueGrey READ getValue) public: - ProgressSpinner(QWidget *parent); + ProgressSpinner(QWidget* parent, int width, int height, + int progressWidth, QColor fgColor, QColor bgColor); ~ProgressSpinner(); inline double getValue() const { return _value; } void setValueGrey(double value); @@ -76,6 +108,8 @@ class ProgressSpinner : public QWidget { int _width; int _height; int _progressWidth; + QColor _fgColor; + QColor _bgColor; double _minimumValue; double _maximumValue; double _value; @@ -89,4 +123,116 @@ class SpinnerPanel : public QWidget { bool background); }; +class SmallSpinner : public QWidget { + Q_OBJECT +public: + SmallSpinner(QWidget* parent); +}; + +enum StepperStepStatus { + PRIOR_STEP, + CURRENT_STEP, + FUTURE_STEP +}; + +class StepperStep : public QWidget { + Q_OBJECT +public: + StepperStep(std::string text, bool firstStep, QWidget* parent); + void setStatus(StepperStepStatus status); +private: + StepperStepStatus _status; + bool _firstStep; + QPixmap _priorMarker, _currentMarker, _futureMarker; + QPixmap _activeSeparator, _inactiveSeparator; + QLabel* _marker; + QLabel* _label; + QLabel* _separator; + + void _update(); +}; + +class Stepper : public QWidget { + Q_OBJECT +public: + Stepper(std::vector stepLabels, QWidget* parent); + + void setStep(int newStep); + void incrementStep(); + void decrementStep(); +private: + int _currentStep = 0; + QListWidget* _list; + QVector _steps; + void _update(); +}; + +class RoundedImageLabel : public QLabel +{ + Q_OBJECT + +public: + explicit RoundedImageLabel(int cornerRadius, QWidget* parent = nullptr); + void setImage(const QPixmap& pixmap); + +protected: + void resizeEvent(QResizeEvent* event) override; + void paintEvent(QPaintEvent* event) override; + +private: + QPixmap originalPixmap; + QPixmap scaledPixmapWithRoundedCorners; + void updateScaledPixmap(); + int _cornerRadius; +}; + +class CameraPlaceholder : public QWidget { + Q_OBJECT + +public: + explicit CameraPlaceholder(int cornerRadius, QWidget* parent = nullptr); + void setIcon(const QString& svgFilePath); + +protected: + void resizeEvent(QResizeEvent* event) override; + void paintEvent(QPaintEvent* event) override; + QSize sizeHint() const override; + +private: + QSvgRenderer* _svgRenderer; + QSize _iconSize; + int _cornerRadius; +}; + +class InfoLabel : public QWidget { + Q_OBJECT + +public: + explicit InfoLabel(const QString& text = "", const QString& svgPath = "", QWidget* parent = nullptr); + void setText(const QString& text); + void setIconFromSvg(const QString& svgPath); // Updated + +protected: + void paintEvent(QPaintEvent* event) override; + +private: + QLabel* _iconLabel; + QLabel* _textLabel; + + QPixmap _renderSvgToPixmap(const QString& svgPath, const QSize& size); +}; + +class StreamPackageHeader : public QWidget { + Q_OBJECT +public: + StreamPackageHeader(QWidget* parent, std::string name, + std::string thumbnailPath); +}; + +class IconLabel : public QWidget { + Q_OBJECT +public: + IconLabel(const std::string& svgPath, const std::string& name, QWidget* parent); +}; + } // namespace elgatocloud diff --git a/src/export-wizard.cpp b/src/export-wizard.cpp index 724418a..d62bdbf 100644 --- a/src/export-wizard.cpp +++ b/src/export-wizard.cpp @@ -30,6 +30,14 @@ with this program. If not, see #include #include #include +#include +#include +#include +#include +#include + +#include "obs-utils.hpp" +#include "util.h" namespace elgatocloud { @@ -37,12 +45,14 @@ SceneBundle *bundle = new SceneBundle(); SceneBundleStatus createBundle(std::string filename, std::vector plugins, + std::vector> thirdParty, + std::vector outputScenes, std::map vidDevLabels) { if (!bundle) { return SceneBundleStatus::InvalidBundle; } - return bundle->ToElgatoCloudFile(filename, plugins, vidDevLabels); + return bundle->ToElgatoCloudFile(filename, plugins, thirdParty, outputScenes, vidDevLabels); } // TODO: For MacOS the filename sigatures will be different @@ -57,75 +67,339 @@ const std::vector ExcludedModules{ "frontend-tools.dll", "decklink-output-ui.dll", "decklink-captions.dll", "coreaudio-encoder.dll", "elgato-marketplace.dll"}; -FileCollectionCheck::FileCollectionCheck(QWidget *parent, - std::vector files) - : QWidget(parent), - _files(files) +// Helper to get icon path based on file extension +QString iconForExtension(const QString& extension) { + static const QMap extensionIconMap = { + {"jpg", "IconImage.svg"}, {"jpeg", "IconImage.svg"}, + {"gif", "IconImage.svg"}, {"png", "IconImage.svg"}, + {"bmp", "IconImage.svg"}, {"webm", "IconVideo.svg"}, + {"mov", "IconVideo.svg"}, {"mp4", "IconVideo.svg"}, + {"mkv", "IconVideo.svg"}, {"mp3", "IconVolume.svg"}, + {"wav", "IconVolume.svg"}, {"effect", "IconFile.svg"}, + {"shader", "IconFile.svg"}, {"hlsl", "IconFile.svg"}, + {"lua", "IconLogoLua.svg"}, {"py", "IconFile.svg"}, + {"html", "IconLogoHtml.svg"}, {"htm", "IconLogoHtml.svg"}, + {"js", "IconLogoJs.svg"}, {"css", "IconLogoCss.svg"} + }; + + QString ext = extension.toLower(); + return extensionIconMap.contains(ext) ? extensionIconMap[ext] : "IconFile.svg"; +} + + +ExportStepsSideBar::ExportStepsSideBar(std::string name, QWidget* parent) + : QWidget(parent) { - QVBoxLayout *layout = new QVBoxLayout(this); + QVBoxLayout* layout = new QVBoxLayout(this); + //auto header = new StreamPackageHeader(this, name, thumbnailPath); + setFixedWidth(240); - auto title = new QLabel(this); - title->setText(obs_module_text("ExportWizard.MediaFileCheck.Title")); + // TODO- translations for steps + std::vector steps = { "Get started", "Collect media files", "Select Output Scenes", "Bundle required plugins", "3rd Party Requirements", "Rename video sources"}; + + std::string titleString = obs_module_text("ExportWizard.Export"); + titleString += " " + name; + + auto header = new StreamPackageHeader(this, titleString, ""); + + _stepper = new Stepper(steps, this); + layout->setSpacing(16); + layout->setContentsMargins(8, 0, 24, 0); + layout->addWidget(header); + layout->addWidget(_stepper); + layout->addStretch(); +} + +void ExportStepsSideBar::setStep(int step) +{ + _stepper->setStep(step); +} + +void ExportStepsSideBar::incrementStep() +{ + _stepper->incrementStep(); +} + +void ExportStepsSideBar::decrementStep() +{ + _stepper->decrementStep(); +} + +StartExport::StartExport(std::string name, QWidget* parent) + : QWidget(parent) +{ + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setSpacing(16); + layout->addStretch(); + + auto placeholder = new CameraPlaceholder(8, this); + std::string imageBaseDir = GetDataPath(); + imageBaseDir += "/images/"; + std::string imgPath = imageBaseDir + "icon-scene-collection.svg"; + placeholder->setIcon(imgPath.c_str()); + placeholder->setFixedWidth(320); + + auto placeholderLayout = new QHBoxLayout(); + placeholderLayout->addStretch(); + placeholderLayout->addWidget(placeholder); + placeholderLayout->addStretch(); + layout->addLayout(placeholderLayout); + + std::string titleString = obs_module_text("ExportWizard.ExportTitle"); + replace_all(titleString, "{COLLECTION_NAME}", name); + //titleString += " " + name; + auto title = new QLabel(titleString.c_str(), this); + title->setStyleSheet(EWizardStepTitle); title->setAlignment(Qt::AlignCenter); - title->setStyleSheet("QLabel{ font-size: 18pt; font-weight: bold;}"); - layout->addWidget(title); - layout->setSpacing(20); - std::string titleText = - std::to_string(_files.size()) + - obs_module_text("ExportWizard.MediaFileCheck.Text"); - auto subTitle = new QLabel(this); - subTitle->setText(titleText.c_str()); - subTitle->setStyleSheet("QLabel {font-size: 12pt;}"); + title->setFixedWidth(320); + title->setWordWrap(true); + + auto titleLayout = new QHBoxLayout(); + titleLayout->addStretch(); + titleLayout->addWidget(title); + titleLayout->addStretch(); + layout->addLayout(titleLayout); + + auto subTitle = new QLabel(obs_module_text("ExportWizard.StartExport.Description"), this); + subTitle->setStyleSheet(EWizardStepSubTitle); + subTitle->setFixedWidth(320); subTitle->setAlignment(Qt::AlignCenter); subTitle->setWordWrap(true); - layout->addWidget(subTitle); - auto fileList = new QListWidget(this); - std::vector browserSourceDirs; - - for (auto fileName : _files) { - bool hasExtension = fileName.rfind(".") != std::string::npos; - std::string extension = - hasExtension ? os_get_path_extension(fileName.c_str()) - : ""; - if (extension == ".html" || extension == ".htm") { - if (fileName.rfind("/") == std::string::npos) { - obs_log(LOG_INFO, "Error exporting file- could not determine parent directory of file."); - continue; - } - std::string parentDir = fileName.substr(0, fileName.rfind("/")); - if (std::find(browserSourceDirs.begin(), browserSourceDirs.end(), parentDir) == browserSourceDirs.end()) { - browserSourceDirs.push_back(parentDir); - std::vector parentDirFiles; - _SubFiles(parentDirFiles, parentDir); - for (auto parentFileName : parentDirFiles) { - fileList->addItem(parentFileName.c_str()); + auto subTitleLayout = new QHBoxLayout(); + subTitleLayout->addStretch(); + subTitleLayout->addWidget(subTitle); + subTitleLayout->addStretch(); + layout->addLayout(subTitleLayout); + + QHBoxLayout* buttons = new QHBoxLayout(); + QPushButton* continueButton = new QPushButton(this); + continueButton->setText(obs_module_text("ExportWizard.GetStartedButton")); + continueButton->setStyleSheet(EWizardButtonStyle); + + connect(continueButton, &QPushButton::released, this, + [this]() { emit continuePressed(); }); + buttons->addStretch(); + buttons->addWidget(continueButton); + buttons->addStretch(); + layout->addLayout(buttons); + layout->addStretch(); +} + +SceneCollectionFilesDelegate::SceneCollectionFilesDelegate(QObject* parent) + : QStyledItemDelegate(parent) { +} + +QString SceneCollectionFilesDelegate::elideMiddlePath(const QString& fullPath, const QFontMetrics& metrics, int maxWidth) const { + QFileInfo fileInfo(fullPath); + QString drivePrefix = fullPath.section("/", 0, 0) + "/"; // e.g., "C:/" + QString suffix = fileInfo.fileName(); // e.g., "file.jpg" + + QStringList parts = fullPath.split('/'); + if (parts.size() < 2) + return fullPath; + + // Reconstruct from the back, keeping the filename and as many folders as fit + QStringList suffixParts; + suffixParts.prepend(suffix); + int baseWidth = metrics.horizontalAdvance(drivePrefix + "/.../"); // guaranteed fixed prefix + int availableWidth = maxWidth - baseWidth; + + int usedWidth = metrics.horizontalAdvance(suffix); + for (int i = parts.size() - 2; i > 0 && usedWidth < availableWidth; --i) { + QString part = parts[i]; + int partWidth = metrics.horizontalAdvance("/" + part); + if (usedWidth + partWidth > availableWidth) + break; + suffixParts.prepend(part); + usedWidth += partWidth; + } + + return drivePrefix + "..." + "/" + suffixParts.join("/"); +} + +void SceneCollectionFilesDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const +{ + painter->save(); + + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + + const QWidget* widget = opt.widget; + QStyle* style = widget ? widget->style() : QApplication::style(); + + style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, widget); + + QRect iconRect = style->subElementRect(QStyle::SE_ItemViewItemDecoration, &opt, widget); + QPixmap pixmap = opt.icon.pixmap(opt.decorationSize); + painter->drawPixmap(iconRect.topLeft(), pixmap); + + QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &opt, widget); + QString elided = elideMiddlePath(opt.text, painter->fontMetrics(), textRect.width()); + style->drawItemText(painter, textRect, opt.displayAlignment, opt.palette, true, elided, QPalette::Text); + + painter->restore(); +} + +QSize SceneCollectionFilesDelegate::sizeHint(const QStyleOptionViewItem& option, + const QModelIndex& index) const +{ + QSize base = QStyledItemDelegate::sizeHint(option, index); + base.setHeight(qMax(base.height(), option.decorationSize.height())); + return base; +} + + + +FileCollectionCheck::FileCollectionCheck(std::string name, + std::vector files, QWidget* parent) + : QWidget(parent), + _files(files) +{ + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + auto main = new QHBoxLayout(); + main->setAlignment(Qt::AlignTop); + main->setContentsMargins(0, 0, 0, 0); + auto sideBar = new ExportStepsSideBar(name, this); + sideBar->setContentsMargins(0, 0, 0, 0); + sideBar->setFixedWidth(240); + sideBar->setStep(1); + + auto form = new QVBoxLayout(); + + auto title = new QLabel(this); + std::string titleLookup = _files.size() == 1 ? "ExportWizard.MediaFileCheck.TitleSingular" : "ExportWizard.MediaFileCheck.TitlePlural"; + const char* subtitleLookup = _files.size() == 1 ? "ExportWizard.MediaFileCheck.SubtitleSingular" : "ExportWizard.MediaFileCheck.SubtitlePlural"; + + if (_files.size() > 0) { + std::string imageBaseDir = GetDataPath(); + imageBaseDir += "/images/"; + //std::string iconPath = imageBaseDir + "IconFile.svg"; + + auto fileList = new QListWidget(this); + fileList->setItemDelegate(new SceneCollectionFilesDelegate(fileList)); + fileList->setIconSize(QSize(20, 20)); + fileList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + fileList->setTextElideMode(Qt::ElideNone); + fileList->setWordWrap(false); + + std::vector browserSourceDirs; + + for (auto fileName : _files) { + bool hasExtension = fileName.rfind(".") != std::string::npos; + std::string extension = + hasExtension ? os_get_path_extension(fileName.c_str()) + : ""; + if (extension == ".html" || extension == ".htm") { + if (fileName.rfind("/") == std::string::npos) { + obs_log(LOG_INFO, "Error exporting file- could not determine parent directory of file."); + continue; } + std::string parentDir = fileName.substr(0, fileName.rfind("/")); + if (std::find(browserSourceDirs.begin(), browserSourceDirs.end(), parentDir) == browserSourceDirs.end()) { + browserSourceDirs.push_back(parentDir); + std::vector parentDirFiles; + _SubFiles(parentDirFiles, parentDir); + for (auto parentFileName : parentDirFiles) { + QFileInfo info(parentFileName.c_str()); + QString extension = info.suffix(); + std::string iconPath = imageBaseDir + iconForExtension(extension).toStdString(); + QListWidgetItem* item = new QListWidgetItem(QIcon(iconPath.c_str()), parentFileName.c_str()); + //fileList->addItem(parentFileName.c_str()); + item->setToolTip(parentFileName.c_str()); + fileList->addItem(item); + } + } + } + else { + QFileInfo info(fileName.c_str()); + QString extension = info.suffix(); + std::string iconPath = imageBaseDir + iconForExtension(extension).toStdString(); + + QListWidgetItem* item = new QListWidgetItem(QIcon(iconPath.c_str()), fileName.c_str()); + //fileList->addItem(fileName.c_str()); + item->setToolTip(fileName.c_str()); + fileList->addItem(item); } - } else { - fileList->addItem(fileName.c_str()); } + int includedCount = fileList->count(); + //fileList->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + fileList->setStyleSheet(EListStyle); + fileList->setSpacing(4); + fileList->setSelectionMode(QAbstractItemView::NoSelection); + fileList->setFocusPolicy(Qt::FocusPolicy::NoFocus); + std::string titleText = obs_module_text(titleLookup.c_str()); + replace_all(titleText, "{COUNT}", std::to_string(includedCount)); + title->setText(titleText.c_str()); + title->setStyleSheet(EWizardStepTitle); + std::string subtitleText = obs_module_text(subtitleLookup); + auto subTitle = new QLabel(this); + subTitle->setText(subtitleText.c_str()); + subTitle->setStyleSheet(EWizardStepSubTitle); + subTitle->setWordWrap(true); + + form->addWidget(title); + form->addWidget(subTitle); + form->addWidget(fileList); + } else { + form->addStretch(); + std::string titleText = obs_module_text(titleLookup.c_str()); + replace_all(titleText, "{COUNT}", std::to_string(0)); + title->setText(titleText.c_str()); + title->setStyleSheet(EWizardStepTitle); + title->setAlignment(Qt::AlignCenter); + + auto subTitle = new QLabel(this); + subTitle->setText(obs_module_text("ExportWizard.MediaFileCheck.SubtitleNone")); + subTitle->setStyleSheet(EWizardStepSubTitle); + subTitle->setWordWrap(true); + subTitle->setAlignment(Qt::AlignCenter); + + auto iconLayout = new QHBoxLayout(); + + QSize iconSize(48, 48); // Set desired icon size + QPixmap pixmap(iconSize); + pixmap.fill(Qt::transparent); // Ensure transparent background + + std::string imageBaseDir = GetDataPath(); + imageBaseDir += "/images/"; + std::string imgPath = imageBaseDir + "icon-checkmark-circle.svg"; + + QSvgRenderer renderer(QString(imgPath.c_str()), this); + QPainter painter(&pixmap); + renderer.render(&painter); + + // Display it in a QLabel + QLabel* iconLabel = new QLabel; + iconLabel->setPixmap(pixmap); + iconLabel->setFixedSize(iconSize); + iconLayout->addStretch(); + iconLayout->addWidget(iconLabel); + iconLayout->addStretch(); + + form->addLayout(iconLayout); + form->addWidget(title); + form->addWidget(subTitle); + form->addStretch(); } - fileList->setStyleSheet(EListStyle); - fileList->setSpacing(4); - fileList->setSelectionMode(QAbstractItemView::NoSelection); - fileList->setFocusPolicy(Qt::FocusPolicy::NoFocus); - - layout->addWidget(fileList); + //form->addStretch(); + main->addWidget(sideBar); + main->addLayout(form); - QHBoxLayout *buttons = new QHBoxLayout(this); + QHBoxLayout *buttons = new QHBoxLayout(); QPushButton *continueButton = new QPushButton(this); continueButton->setText(obs_module_text("ExportWizard.NextButton")); - continueButton->setStyleSheet(EPushButtonStyle); + continueButton->setStyleSheet(EWizardButtonStyle); connect(continueButton, &QPushButton::released, this, [this]() { emit continuePressed(); }); - auto spacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - buttons->addWidget(spacer); + buttons->addStretch(); buttons->addWidget(continueButton); + layout->addLayout(main); layout->addLayout(buttons); } @@ -163,49 +437,78 @@ bool FileCollectionCheck::_SubFiles(std::vector& files, std::string return true; } -VideoSourceLabels::VideoSourceLabels(QWidget *parent, - std::map devices) +VideoSourceLabels::VideoSourceLabels(std::string name, + std::map devices, QWidget* parent) : QWidget(parent) { QVBoxLayout *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + auto main = new QHBoxLayout(); + main->setAlignment(Qt::AlignTop); + main->setContentsMargins(0, 0, 0, 0); + auto sideBar = new ExportStepsSideBar(name, this); + sideBar->setContentsMargins(0, 0, 0, 0); + sideBar->setFixedWidth(240); + sideBar->setStep(5); - std::string titleText = obs_module_text("ExportWizard.VideoSourceLabels.Title"); - auto title = new QLabel(this); - title->setAlignment(Qt::AlignCenter); - title->setStyleSheet(ETitleStyle); - title->setText(titleText.c_str()); - layout->setSpacing(20); - layout->addWidget(title); - - auto description = new QLabel(this); - description->setText( - obs_module_text("ExportWizard.VideoSourceLabels.Text")); - description->setStyleSheet( - "QLabel {font-size: 12pt; margin-bottom: 10px;}"); - description->setAlignment(Qt::AlignCenter); - layout->addWidget(description); + auto formLayout = new QVBoxLayout(); - // Declare continue button so we can enable/disable it - // on keypress in label list. auto continueButton = new QPushButton(this); - auto formLayout = new QVBoxLayout(); formLayout->setSpacing(4); if (devices.size() > 0) { - continueButton->setDisabled(true); + auto title = new QLabel(obs_module_text("ExportWizard.VideoSourceLabels.Title"), this); + title->setStyleSheet(EWizardStepTitle); + auto subTitle = new QLabel(obs_module_text("ExportWizard.VideoSourceLabel.SubTitle"), this); + subTitle->setStyleSheet(EWizardStepSubTitle); + subTitle->setWordWrap(true); + std::string imageBaseDir = GetDataPath(); + imageBaseDir += "/images/"; + std::string cameraIconPath = imageBaseDir + "icon-camera.svg"; + std::string arrowIconPath = imageBaseDir + "icon-arrow-right.svg"; + + // TODO: Translation files for these two fields + auto videoGrid = new QGridLayout; + videoGrid->setContentsMargins(0, 16, 0, 0); + videoGrid->setSpacing(12); + auto currentLabel = new QLabel("Current", this); + currentLabel->setStyleSheet(EWizardFieldLabel); + auto newLabel = new QLabel("New", this); + newLabel->setStyleSheet(EWizardFieldLabel); + + videoGrid->addWidget(currentLabel, 0, 0, Qt::AlignLeft); + videoGrid->addWidget(newLabel, 0, 2); + int i = 1; + int row = 1; for (auto const &[key, val] : devices) { - auto label = new QLabel(val.c_str(), this); - label->setStyleSheet( - "QLabel {font-size: 12pt; padding: 12px 0px 8px 0px; }"); + auto label = new IconLabel(cameraIconPath, val.c_str(), this); - auto field = new QLineEdit(this); + QLabel* arrow = new QLabel(this); + QSize iconSize(20, 20); + QPixmap pixmap(iconSize); + pixmap.fill(Qt::transparent); + + QSvgRenderer renderer(QString(arrowIconPath.c_str())); + QPainter painter(&pixmap); + renderer.render(&painter); + + arrow->setPixmap(pixmap); + arrow->setFixedSize(iconSize); + + auto field = new QLineEdit(val.c_str(), this); field->setPlaceholderText( obs_module_text("ExportWizard.VideoSourceLabels.InputPlaceholder")); field->setMaxLength(32); - field->setStyleSheet(ELineEditStyle); - formLayout->addWidget(label); - formLayout->addWidget(field); - _labels[val] = ""; + field->setStyleSheet(EWizardTextField); + + + videoGrid->addWidget(label, row, 0, Qt::AlignLeft); + videoGrid->addWidget(arrow, row, 1, Qt::AlignCenter); + videoGrid->addWidget(field, row, 2); + + //formLayout->addWidget(label); + //formLayout->addWidget(field); + _labels[val] = val.c_str(); connect(field, &QLineEdit::textChanged, this, [this, val, continueButton](const QString &text) { @@ -219,52 +522,194 @@ VideoSourceLabels::VideoSourceLabels(QWidget *parent, } continueButton->setDisabled(!enable); }); + row++; } - layout->addLayout(formLayout); + formLayout->addWidget(title); + formLayout->addWidget(subTitle); + formLayout->addLayout(videoGrid); } else { continueButton->setDisabled(false); - auto topSpace = new QWidget(this); - topSpace->setSizePolicy(QSizePolicy::Preferred, - QSizePolicy::Expanding); - layout->addWidget(topSpace); + formLayout->addStretch(); auto noVCDevices = new QLabel( obs_module_text("ExportWizard.VideoSourceLabels.NoCaptureSourcesText"), this); noVCDevices->setAlignment(Qt::AlignCenter); - noVCDevices->setStyleSheet("QLabel{font-size: 18pt;}"); - layout->addWidget(noVCDevices); + noVCDevices->setStyleSheet(EWizardStepTitle); + + auto iconLayout = new QHBoxLayout(); + + QSize iconSize(48, 48); // Set desired icon size + QPixmap pixmap(iconSize); + pixmap.fill(Qt::transparent); // Ensure transparent background + + std::string imageBaseDir = GetDataPath(); + imageBaseDir += "/images/"; + std::string imgPath = imageBaseDir + "icon-checkmark-circle.svg"; + + QSvgRenderer renderer(QString(imgPath.c_str()), this); + QPainter painter(&pixmap); + renderer.render(&painter); + + // Display it in a QLabel + QLabel* iconLabel = new QLabel; + iconLabel->setPixmap(pixmap); + iconLabel->setFixedSize(iconSize); + iconLayout->addStretch(); + iconLayout->addWidget(iconLabel); + iconLayout->addStretch(); + + auto noneLayout = new QHBoxLayout(); + + + auto noVCDevicesSub = new QLabel(obs_module_text("ExportWizard.VideoSourceLabels.NoCaptureSourcesSub"), this); + noVCDevicesSub->setAlignment(Qt::AlignCenter); + noVCDevicesSub->setStyleSheet(EWizardStepSubTitle); + formLayout->addLayout(iconLayout); + formLayout->addWidget(noVCDevices); + formLayout->addWidget(noVCDevicesSub); } - auto spacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + formLayout->addStretch(); - layout->addWidget(spacer); + auto buttons = new QHBoxLayout(); + auto backButton = new QPushButton(this); + backButton->setText( + obs_module_text("ExportWizard.BackButton")); + backButton->setStyleSheet(EWizardQuietButtonStyle); + connect(backButton, &QPushButton::released, this, + [this]() { emit backPressed(); }); + + continueButton->setText( + obs_module_text("ExportWizard.NextButton")); + continueButton->setStyleSheet(EWizardButtonStyle); + + connect(continueButton, &QPushButton::released, this, + [this]() { emit continuePressed(); }); + buttons->addStretch(); + buttons->addWidget(backButton); + buttons->addWidget(continueButton); + main->addWidget(sideBar); + main->addLayout(formLayout); + layout->addLayout(main); + layout->addLayout(buttons); +} - auto buttonSpacer = new QWidget(this); - buttonSpacer->setSizePolicy(QSizePolicy::Expanding, - QSizePolicy::Preferred); - auto buttons = new QHBoxLayout(this); +SelectOutputScenes::SelectOutputScenes(std::string name, QWidget* parent) + : QWidget(parent) +{ + obs_enum_scenes(SelectOutputScenes::AddScene, this); + std::sort(_scenes.begin(), _scenes.end(), [](const SceneInfo& a, const SceneInfo& b) { + return a.name < b.name; + }); + + std::string imagesPath = getImagesPath(); + std::string checkedImage = imagesPath + "checkbox_checked.png"; + std::string uncheckedImage = imagesPath + "checkbox_unchecked.png"; + QString checklistStyle = EWizardChecklistStyle; + checklistStyle.replace("${checked-img}", checkedImage.c_str()); + checklistStyle.replace("${unchecked-img}", uncheckedImage.c_str()); + + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + auto main = new QHBoxLayout(); + main->setAlignment(Qt::AlignTop); + main->setContentsMargins(0, 0, 0, 0); + auto sideBar = new ExportStepsSideBar(name, this); + sideBar->setContentsMargins(0, 0, 0, 0); + sideBar->setFixedWidth(240); + sideBar->setStep(2); + + auto formLayout = new QVBoxLayout(); + + + auto title = new QLabel(obs_module_text("ExportWizard.OutputScenes.Title"), this); + title->setStyleSheet(EWizardStepTitle); + + auto subTitle = new QLabel(this); + subTitle->setText(obs_module_text("ExportWizard.OutputScenes.Subtitle")); + subTitle->setStyleSheet(EWizardStepSubTitle); + + auto sceneList = new QListWidget(this); + sceneList->setSizePolicy(QSizePolicy::Preferred, + QSizePolicy::Expanding); + sceneList->setSpacing(8); + sceneList->setStyleSheet(checklistStyle); + sceneList->setSelectionMode(QAbstractItemView::NoSelection); + sceneList->setFocusPolicy(Qt::FocusPolicy::NoFocus); + + for (auto scene : _scenes) { + sceneList->addItem(scene.name.c_str()); + } + + QListWidgetItem* item = nullptr; + for (int i = 0; i < sceneList->count(); ++i) { + item = sceneList->item(i); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + } + + connect(sceneList, &QListWidget::itemChanged, this, + [this, sceneList](QListWidgetItem* item) { + int index = sceneList->row(item); + bool outputScene = item->checkState() == Qt::Checked; + _scenes[index].outputScene = outputScene; + }); + + formLayout->addWidget(title); + formLayout->addWidget(subTitle); + formLayout->addWidget(sceneList); + + auto buttons = new QHBoxLayout(); auto backButton = new QPushButton(this); backButton->setText( obs_module_text("ExportWizard.BackButton")); - backButton->setStyleSheet(EPushButtonStyle); + backButton->setStyleSheet(EWizardQuietButtonStyle); connect(backButton, &QPushButton::released, this, [this]() { emit backPressed(); }); + auto continueButton = new QPushButton(this); continueButton->setText( obs_module_text("ExportWizard.NextButton")); - continueButton->setStyleSheet(EPushButtonStyle); + continueButton->setStyleSheet(EWizardButtonStyle); connect(continueButton, &QPushButton::released, this, [this]() { emit continuePressed(); }); - buttons->addWidget(buttonSpacer); + buttons->addStretch(); buttons->addWidget(backButton); buttons->addWidget(continueButton); + + main->addWidget(sideBar); + main->addLayout(formLayout); + layout->addLayout(main); layout->addLayout(buttons); } -RequiredPlugins::RequiredPlugins(QWidget *parent, - std::vector installedPlugins) +bool SelectOutputScenes::AddScene(void* data, obs_source_t* scene) +{ + auto instance = static_cast(data); + if (obs_source_is_group(scene)) { + return true; + } + std::string name = obs_source_get_name(scene); + std::string id = obs_source_get_uuid(scene); + SceneInfo info = { name, id, false }; + instance->_scenes.push_back(info); + return true; +} + +std::vector SelectOutputScenes::OutputScenes() const +{ + std::vector output; + for (auto const& scene : _scenes) { + if (scene.outputScene) { + output.push_back(scene); + } + } + return output; +} + +RequiredPlugins::RequiredPlugins(std::string name, + std::vector installedPlugins, QWidget* parent) : QWidget(parent) { PluginInfo pi; @@ -272,32 +717,41 @@ RequiredPlugins::RequiredPlugins(QWidget *parent, auto installed = pi.installed(); // Set up the paths for checked/unchecked images. // TODO: Refactor this, there must be a better way. - std::string imagesPath = obs_get_module_data_path(obs_current_module()); - imagesPath += "/images/"; + //std::string imagesPath = obs_get_module_data_path(obs_current_module()); + //imagesPath += "/images/"; + std::string imagesPath = getImagesPath(); std::string checkedImage = imagesPath + "checkbox_checked.png"; std::string uncheckedImage = imagesPath + "checkbox_unchecked.png"; - QString checklistStyle = EChecklistStyleTemplate; + QString checklistStyle = EWizardChecklistStyle; + obs_log(LOG_INFO, "checked image: %s", checkedImage.c_str()); checklistStyle.replace("${checked-img}", checkedImage.c_str()); checklistStyle.replace("${unchecked-img}", uncheckedImage.c_str()); + obs_log(LOG_INFO, "style: %s", checklistStyle.toStdString().c_str()); - QVBoxLayout *layout = new QVBoxLayout(this); - std::string titleText = obs_module_text("ExportWizard.RequiredPlugins.Title"); - auto title = new QLabel(this); - title->setText(titleText.c_str()); - title->setAlignment(Qt::AlignCenter); - title->setStyleSheet(ETitleStyle); - layout->addWidget(title); - layout->setSpacing(20); + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + auto main = new QHBoxLayout(); + main->setAlignment(Qt::AlignTop); + main->setContentsMargins(0, 0, 0, 0); + auto sideBar = new ExportStepsSideBar(name, this); + sideBar->setContentsMargins(0, 0, 0, 0); + sideBar->setFixedWidth(240); + sideBar->setStep(3); - auto subTitle = new QLabel(this); - subTitle->setText( - obs_module_text("ExportWizard.RequiredPlugins.Text")); - subTitle->setStyleSheet("QLabel {font-size: 12pt;}"); - subTitle->setAlignment(Qt::AlignCenter); - subTitle->setWordWrap(true); - layout->addWidget(subTitle); + auto formLayout = new QVBoxLayout(); if (installed.size() > 0) { + std::string titleText = installed.size() == 1 ? obs_module_text("ExportWizard.RequiredPlugins.SingleTitle") : obs_module_text("ExportWizard.RequiredPlugins.PluralTitle"); + + replace_all(titleText, "{COUNT}", std::to_string(installed.size())); + + auto title = new QLabel(titleText.c_str(), this); + title->setStyleSheet(EWizardStepTitle); + + auto subTitle = new QLabel(this); + subTitle->setText(obs_module_text("ExportWizard.RequiredPlugins.SubTitle")); + subTitle->setStyleSheet(EWizardStepSubTitle); + auto pluginList = new QListWidget(this); pluginList->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); @@ -329,79 +783,270 @@ RequiredPlugins::RequiredPlugins(QWidget *parent, item->checkState() == Qt::Checked; }); - layout->addWidget(pluginList); + formLayout->addWidget(title); + formLayout->addWidget(subTitle); + formLayout->addWidget(pluginList); } else { auto topSpace = new QWidget(this); topSpace->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); - layout->addWidget(topSpace); + formLayout->addWidget(topSpace); auto noPlugins = new QLabel( obs_module_text("ExportWizard.RequiredPlugins.NoPluginsFound"), this); noPlugins->setAlignment(Qt::AlignCenter); noPlugins->setStyleSheet("QLabel{font-size: 18pt;}"); - layout->addWidget(noPlugins); - layout->addStretch(); + formLayout->addWidget(noPlugins); + formLayout->addStretch(); } - auto buttons = new QHBoxLayout(this); - auto buttonSpacer = new QWidget(this); - buttonSpacer->setSizePolicy(QSizePolicy::Expanding, - QSizePolicy::Preferred); + auto buttons = new QHBoxLayout(); auto backButton = new QPushButton(this); backButton->setText( obs_module_text("ExportWizard.BackButton")); - backButton->setStyleSheet(EPushButtonStyle); + backButton->setStyleSheet(EWizardQuietButtonStyle); connect(backButton, &QPushButton::released, this, [this]() { emit backPressed(); }); auto continueButton = new QPushButton(this); continueButton->setText( obs_module_text("ExportWizard.NextButton")); - continueButton->setStyleSheet(EPushButtonStyle); + continueButton->setStyleSheet(EWizardButtonStyle); connect(continueButton, &QPushButton::released, this, [this]() { emit continuePressed(); }); - buttons->addWidget(buttonSpacer); + buttons->addStretch(); buttons->addWidget(backButton); buttons->addWidget(continueButton); + + main->addWidget(sideBar); + main->addLayout(formLayout); + layout->addLayout(main); layout->addLayout(buttons); } -ExportComplete::ExportComplete(QWidget *parent) : QWidget(parent) +ThirdPartyRequirements::ThirdPartyRequirements(std::string name, QWidget* parent) + : QWidget(parent) { - QVBoxLayout *layout = new QVBoxLayout(this); + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + auto main = new QHBoxLayout(); + main->setAlignment(Qt::AlignTop); + main->setContentsMargins(0, 0, 0, 0); + auto sideBar = new ExportStepsSideBar(name, this); + sideBar->setContentsMargins(0, 0, 0, 0); + sideBar->setFixedWidth(240); + sideBar->setStep(4); - std::string titleText = obs_module_text("ExportWizard.ExportComplete.Title"); - auto title = new QLabel(this); - title->setText(titleText.c_str()); - title->setStyleSheet(ETitleStyle); - layout->addWidget(title); - std::string subText = - obs_module_text("ExportWizard.ExportComplete.Text"); - auto sub = new QLabel(this); - sub->setText(subText.c_str()); - sub->setStyleSheet("QLabel{ font-size: 11pt; font-style: italic; }"); - sub->setWordWrap(true); - layout->addWidget(sub); + std::string titleText = obs_module_text("ExportWizard.ThirdPartyRequirements.Title"); + auto title = new QLabel(titleText.c_str(), this); + title->setStyleSheet(EWizardStepTitle); + + auto subTitle = new QLabel(this); + subTitle->setText(obs_module_text("ExportWizard.ThirdPartyRequirements.SubTitle")); + subTitle->setStyleSheet(EWizardStepSubTitle); - auto spacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + _formLayout = new QVBoxLayout(); + _formLayout->setAlignment(Qt::AlignTop); + _formLayout->addWidget(title); + _formLayout->addWidget(subTitle); + _formLayout->addStretch(); + addInputRow(); - layout->addWidget(spacer); + auto buttons = new QHBoxLayout(); + auto backButton = new QPushButton(this); + backButton->setText( + obs_module_text("ExportWizard.BackButton")); + backButton->setStyleSheet(EWizardQuietButtonStyle); + connect(backButton, &QPushButton::released, this, + [this]() { emit backPressed(); }); + + auto continueButton = new QPushButton(this); + continueButton->setText( + obs_module_text("ExportWizard.NextButton")); + continueButton->setStyleSheet(EWizardButtonStyle); + + connect(continueButton, &QPushButton::released, this, + [this]() { emit continuePressed(); }); + buttons->addStretch(); + buttons->addWidget(backButton); + buttons->addWidget(continueButton); + + main->addWidget(sideBar); + main->addLayout(_formLayout); + layout->addLayout(main); + layout->addLayout(buttons); +} + +void ThirdPartyRequirements::addInputRow() +{ + QWidget* rowWidget = new QWidget(this); + QHBoxLayout* rowLayout = new QHBoxLayout(rowWidget); + + QLineEdit* titleEdit = new QLineEdit; + QLineEdit* urlEdit = new QLineEdit; + QPushButton* deleteButton = new QPushButton("Delete"); + + titleEdit->setPlaceholderText("Title"); + urlEdit->setPlaceholderText("URL"); + + rowLayout->addWidget(titleEdit); + rowLayout->addWidget(urlEdit); + rowLayout->addWidget(deleteButton); + + rowWidget->setLayout(rowLayout); + _formLayout->insertWidget(_formLayout->count() - 1, rowWidget); + //_formLayout->addWidget(rowWidget); + + InputRow row = { rowWidget, titleEdit, urlEdit, deleteButton }; + _inputRows.append(row); + + connect(titleEdit, &QLineEdit::textChanged, this, &ThirdPartyRequirements::onTextChanged); + connect(urlEdit, &QLineEdit::textChanged, this, &ThirdPartyRequirements::onTextChanged); + connect(deleteButton, &QPushButton::clicked, this, &ThirdPartyRequirements::onDeleteClicked); +} + +bool ThirdPartyRequirements::isLastRow(const InputRow& row) const +{ + return !_inputRows.isEmpty() && _inputRows.last().rowWidget == row.rowWidget; +} + +bool ThirdPartyRequirements::isRowFilled(const InputRow& row) const +{ + return !row.titleEdit->text().isEmpty() && isValidHttpUrl(row.urlEdit->text()); +} + +void ThirdPartyRequirements::removeInputRow(QWidget* rowWidget) +{ + for (int i = 0; i < _inputRows.size(); ++i) { + if (_inputRows[i].rowWidget == rowWidget) { + _formLayout->removeWidget(rowWidget); + _inputRows.removeAt(i); + rowWidget->deleteLater(); + break; + } + } + + // Ensure at least one empty row exists + if (_inputRows.isEmpty() || isRowFilled(_inputRows.last())) { + addInputRow(); + } +} + +void ThirdPartyRequirements::onTextChanged() +{ + if (_inputRows.isEmpty()) + return; - auto buttons = new QHBoxLayout(this); + const InputRow& last = _inputRows.last(); + if (isRowFilled(last)) { + addInputRow(); + } +} + +std::vector> ThirdPartyRequirements::getRequirements() const +{ + std::vector> entries; + + for (const auto& row : _inputRows) { + QString title = row.titleEdit->text().trimmed(); + QString url = row.urlEdit->text().trimmed(); + + if (!title.isEmpty() && !url.isEmpty()) { + entries.emplace_back(title.toStdString(), url.toStdString()); + } + } + + return entries; +} + +static bool isValidHttpUrl(const QString& urlStr) { + QUrl url(urlStr); + return url.isValid() && (url.scheme() == "http" || url.scheme() == "https"); +} + +void ThirdPartyRequirements::onDeleteClicked() +{ + QPushButton* senderButton = qobject_cast(sender()); + if (!senderButton) + return; + + for (int i = 0; i < _inputRows.size(); ++i) { + if (_inputRows[i].deleteButton == senderButton) { + removeInputRow(_inputRows[i].rowWidget); + break; + } + } +} + + + +ExportComplete::ExportComplete(std::string name, QWidget *parent) : QWidget(parent) +{ + + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setSpacing(16); + layout->addStretch(); + + auto placeholder = new CameraPlaceholder(8, this); + std::string imageBaseDir = GetDataPath(); + imageBaseDir += "/images/"; + std::string imgPath = imageBaseDir + "icon-scene-collection.svg"; + placeholder->setIcon(imgPath.c_str()); + placeholder->setFixedWidth(320); + + auto placeholderLayout = new QHBoxLayout(); + placeholderLayout->addStretch(); + placeholderLayout->addWidget(placeholder); + placeholderLayout->addStretch(); + layout->addLayout(placeholderLayout); + + //std::string titleString = obs_module_text("ExportWizard.Export"); + std::string titleString = obs_module_text("ExportWizard.Complete"); + replace_all(titleString, "{COLLECTION_NAME}", name); + //titleString += " " + name + " " + complete; + auto title = new QLabel(titleString.c_str(), this); + title->setStyleSheet(EWizardStepTitle); + title->setAlignment(Qt::AlignCenter); + title->setFixedWidth(320); + title->setWordWrap(true); + + auto titleLayout = new QHBoxLayout(); + titleLayout->addStretch(); + titleLayout->addWidget(title); + titleLayout->addStretch(); + layout->addLayout(titleLayout); + + auto subTitle = new QLabel(obs_module_text("ExportWizard.ExportComplete.Text"), this); + subTitle->setStyleSheet(EWizardStepSubTitle); + subTitle->setFixedWidth(320); + subTitle->setAlignment(Qt::AlignCenter); + subTitle->setWordWrap(true); + + auto subTitleLayout = new QHBoxLayout(); + subTitleLayout->addStretch(); + subTitleLayout->addWidget(subTitle); + subTitleLayout->addStretch(); + layout->addLayout(subTitleLayout); + + QHBoxLayout* buttons = new QHBoxLayout(); auto closeButton = new QPushButton(this); closeButton->setText( obs_module_text("ExportWizard.CloseButton")); - closeButton->setStyleSheet(EPushButtonStyle); + closeButton->setStyleSheet(EWizardButtonStyle); + connect(closeButton, &QPushButton::released, this, + [this]() { emit closePressed(); }); + connect(closeButton, &QPushButton::released, this, [this]() { emit closePressed(); }); + buttons->addStretch(); buttons->addWidget(closeButton); + buttons->addStretch(); layout->addLayout(buttons); + layout->addStretch(); } std::vector RequiredPlugins::RequiredPluginList() @@ -415,7 +1060,7 @@ std::vector RequiredPlugins::RequiredPluginList() return reqList; } -Exporting::Exporting(QWidget *parent) : QWidget(parent) +Exporting::Exporting(std::string name, QWidget *parent) : QWidget(parent) { QVBoxLayout *layout = new QVBoxLayout(this); @@ -476,7 +1121,7 @@ void StreamPackageExportWizard::SetupUI() { obs_enum_modules(StreamPackageExportWizard::AddModule, this); setWindowTitle("Elgato Marketplace Scene Collection Export"); - setStyleSheet("background-color: #232323"); + setStyleSheet("background-color: #151515"); std::string homeDir = QDir::homePath().toStdString(); char* currentCollection = obs_frontend_get_current_scene_collection(); @@ -491,83 +1136,115 @@ void StreamPackageExportWizard::SetupUI() bundle->VideoCaptureDevices(); QVBoxLayout* layout = new QVBoxLayout(this); + layout->setContentsMargins(8, 8, 8, 8); _steps = new QStackedWidget(this); + _steps->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - // Step 1- check the media files to be bundled (step index: 0) - auto fileCheck = new FileCollectionCheck(this, fileList); - connect(fileCheck, &FileCollectionCheck::continuePressed, this, + // Step 1- Getting started (step index: 0) + auto startExport = new StartExport(collectionName, this); + connect(startExport, &StartExport::continuePressed, this, [this]() { _steps->setCurrentIndex(1); }); + _steps->addWidget(startExport); + + + // Step 2- check the media files to be bundled (step index: 1) + auto fileCheck = new FileCollectionCheck(collectionName, fileList, this); + connect(fileCheck, &FileCollectionCheck::continuePressed, this, + [this]() { _steps->setCurrentIndex(2); }); connect(fileCheck, &FileCollectionCheck::cancelPressed, this, [this]() { close(); }); _steps->addWidget(fileCheck); - // Step 2- Add labels for each video capture device source (step index: 1) - auto videoLabels = new VideoSourceLabels(this, vidDevs); - connect(videoLabels, &VideoSourceLabels::continuePressed, this, + // Step 3- let maker select output scenes for partial imports + auto outputScenes = new SelectOutputScenes(collectionName, this); + connect(outputScenes, &SelectOutputScenes::continuePressed, this, + [this]() { _steps->setCurrentIndex(3); }); + connect(outputScenes, &SelectOutputScenes::backPressed, this, + [this]() { _steps->setCurrentIndex(1); }); + _steps->addWidget(outputScenes); + + // Step 4- Specify required plugins (step index: 3) + auto reqPlugins = new RequiredPlugins(collectionName, _modules, this); + connect(reqPlugins, &RequiredPlugins::continuePressed, this, [this]() { + _steps->setCurrentIndex(4); + }); + connect(reqPlugins, &RequiredPlugins::backPressed, this, [this]() { _steps->setCurrentIndex(2); }); - connect(videoLabels, &VideoSourceLabels::backPressed, this, - [this]() { _steps->setCurrentIndex(0); }); - _steps->addWidget(videoLabels); + _steps->addWidget(reqPlugins); - // Step 2- Specify required plugins (step index: 2) - auto reqPlugins = new RequiredPlugins(this, _modules); - connect(reqPlugins, &RequiredPlugins::continuePressed, this, [this, videoLabels, reqPlugins]() { - auto vidDevLabels = videoLabels->Labels(); - auto plugins = reqPlugins->RequiredPluginList(); - QWidget* window = (QWidget*)obs_frontend_get_main_window(); - QString filename = QFileDialog::getSaveFileName( - window, "Save As...", QString(), "*.elgatoscene"); - if (filename == "") { - _steps->setCurrentIndex(2); - return; - } - if (!filename.endsWith(".elgatoscene")) { - filename += ".elgatoscene"; - } - _steps->setCurrentIndex(3); - std::string filename_utf8 = filename.toUtf8().constData(); - // This is a bit of threading hell. - // Find a better way to set this up, perhaps in an - // installer handler thread handling object. - _future = - QtConcurrent::run(createBundle, filename_utf8, plugins, - vidDevLabels) - .then([this](SceneBundleStatus status) { - // We are now in a different thread, so we need to execute this - // back in the gui thread. See, threading hell. - QMetaObject::invokeMethod( - QCoreApplication::instance() - ->thread(), // main GUI thread - [this, status]() { - if (status == - SceneBundleStatus:: - Success) { - _steps->setCurrentIndex( - 4); - } - else if ( - status == - SceneBundleStatus:: - Cancelled) { - _steps->setCurrentIndex( - 2); - } - }); - }) - .onCanceled([]() { + // Step 5- Specify required plugins (step index: 4) + auto thirdPartyReqs = new ThirdPartyRequirements(collectionName, this); + connect(thirdPartyReqs, &ThirdPartyRequirements::continuePressed, this, [this]() { + _steps->setCurrentIndex(5); + }); + connect(thirdPartyReqs, &ThirdPartyRequirements::backPressed, this, + [this]() { _steps->setCurrentIndex(3); }); + _steps->addWidget(thirdPartyReqs); + + // Step 6- Add labels for each video capture device source (step index: 5) + auto videoLabels = new VideoSourceLabels(collectionName, vidDevs, this); + connect(videoLabels, &VideoSourceLabels::continuePressed, this, + [this, videoLabels, reqPlugins, thirdPartyReqs, outputScenes]() { + + auto vidDevLabels = videoLabels->Labels(); + auto plugins = reqPlugins->RequiredPluginList(); + auto oScenes = outputScenes->OutputScenes(); + auto thirdParty = thirdPartyReqs->getRequirements(); + + QWidget* window = (QWidget*)obs_frontend_get_main_window(); + QString filename = QFileDialog::getSaveFileName( + window, "Save As...", QString(), "*.elgatoscene"); + if (filename == "") { + _steps->setCurrentIndex(5); + return; + } + if (!filename.endsWith(".elgatoscene")) { + filename += ".elgatoscene"; + } + _steps->setCurrentIndex(6); + std::string filename_utf8 = filename.toUtf8().constData(); + // This is a bit of threading hell. + // Find a better way to set this up, perhaps in an + // installer handler thread handling object. + _future = + QtConcurrent::run(createBundle, filename_utf8, plugins, thirdParty, oScenes, + vidDevLabels) + .then([this](SceneBundleStatus status) { + // We are now in a different thread, so we need to execute this + // back in the gui thread. See, threading hell. + QMetaObject::invokeMethod( + QCoreApplication::instance() + ->thread(), // main GUI thread + [this, status]() { + if (status == + SceneBundleStatus:: + Success) { + _steps->setCurrentIndex(7); + } + else if ( + status == + SceneBundleStatus:: + Cancelled) { + _steps->setCurrentIndex(5); + } + }); + }) + .onCanceled([]() { }); - //createBundle(filename_utf8, plugins, vidDevLabels); }); - connect(reqPlugins, &RequiredPlugins::backPressed, this, - [this]() { _steps->setCurrentIndex(1); }); - _steps->addWidget(reqPlugins); - auto exporting = new Exporting(this); + + connect(videoLabels, &VideoSourceLabels::backPressed, this, + [this]() { _steps->setCurrentIndex(4); }); + _steps->addWidget(videoLabels); + + + auto exporting = new Exporting(collectionName, this); _steps->addWidget(exporting); // Successful Export - auto success = new ExportComplete(this); + auto success = new ExportComplete(collectionName, this); connect(success, &ExportComplete::closePressed, this, [this]() { close(); }); _steps->addWidget(success); diff --git a/src/export-wizard.hpp b/src/export-wizard.hpp index b446e5b..5ebc2c0 100644 --- a/src/export-wizard.hpp +++ b/src/export-wizard.hpp @@ -25,7 +25,7 @@ with this program.If not, see < https://www.gnu.org/licenses/> #include #include #include -#include +#include #include #include @@ -34,16 +34,55 @@ with this program.If not, see < https://www.gnu.org/licenses/> #include #include #include +#include +#include #include "scene-bundle.hpp" +#include "elgato-widgets.hpp" + namespace elgatocloud { +class ExportStepsSideBar : public QWidget { + Q_OBJECT +public: + ExportStepsSideBar(std::string name, QWidget* parent); + void setStep(int step); + void incrementStep(); + void decrementStep(); + +private: + Stepper* _stepper; +}; + +class StartExport : public QWidget { + Q_OBJECT +public: + StartExport(std::string name, QWidget* parent); +signals: + void continuePressed(); +}; + + +class SceneCollectionFilesDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + explicit SceneCollectionFilesDelegate(QObject* parent = nullptr); + + QString elideMiddlePath(const QString& fullPath, const QFontMetrics& metrics, int maxWidth) const; + + void paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const override; + + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; +}; + + class FileCollectionCheck : public QWidget { Q_OBJECT public: - FileCollectionCheck(QWidget *parent, std::vector files); + FileCollectionCheck(std::string name, std::vector files, QWidget* parent); signals: void continuePressed(); @@ -54,11 +93,27 @@ class FileCollectionCheck : public QWidget { bool _SubFiles(std::vector& files, std::string curDir); }; +class SelectOutputScenes : public QWidget { + Q_OBJECT + +public: + SelectOutputScenes(std::string name, QWidget* parent = nullptr); + std::vector OutputScenes() const; + static bool AddScene(void* data, obs_source_t* scene); + +signals: + void continuePressed(); + void backPressed(); + +private: + std::vector _scenes; +}; + class VideoSourceLabels : public QWidget { Q_OBJECT public: - VideoSourceLabels(QWidget *parent, - std::map devices); + VideoSourceLabels(std::string name, + std::map devices, QWidget* parent); inline std::map Labels() { return _labels; } signals: void continuePressed(); @@ -71,8 +126,8 @@ class VideoSourceLabels : public QWidget { class RequiredPlugins : public QWidget { Q_OBJECT public: - RequiredPlugins(QWidget *parent, - std::vector installedPlugins); + RequiredPlugins(std::string name, + std::vector installedPlugins, QWidget* parent); std::vector RequiredPluginList(); signals: void continuePressed(); @@ -82,10 +137,43 @@ class RequiredPlugins : public QWidget { std::map> _pluginStatus; }; +class ThirdPartyRequirements : public QWidget { + Q_OBJECT +public: + ThirdPartyRequirements(std::string name, QWidget* parent); + std::vector> getRequirements() const; + +signals: + void continuePressed(); + void backPressed(); + +private slots: + void onTextChanged(); + void onDeleteClicked(); + +private: + struct InputRow { + QWidget* rowWidget; + QLineEdit* titleEdit; + QLineEdit* urlEdit; + QPushButton* deleteButton; + }; + + QVBoxLayout* _formLayout; + QVector _inputRows; + + void addInputRow(); + void removeInputRow(QWidget* rowWidget); + bool isLastRow(const InputRow& row) const; + bool isRowFilled(const InputRow& row) const; +}; + +static bool isValidHttpUrl(const QString& urlStr); + class Exporting : public QWidget { Q_OBJECT public: - Exporting(QWidget *parent); + Exporting(std::string name, QWidget *parent); ~Exporting(); private: @@ -96,7 +184,7 @@ class ExportComplete : public QWidget { Q_OBJECT public: - ExportComplete(QWidget *parent); + ExportComplete(std::string name, QWidget *parent); signals: void closePressed(); diff --git a/src/qt-display.cpp b/src/qt-display.cpp index 5913766..30155bc 100644 --- a/src/qt-display.cpp +++ b/src/qt-display.cpp @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include diff --git a/src/scene-bundle.cpp b/src/scene-bundle.cpp index b8908a5..2b69247 100644 --- a/src/scene-bundle.cpp +++ b/src/scene-bundle.cpp @@ -24,9 +24,11 @@ with this program. If not, see #include #include #include +#include #include #include #include +#include #include #include #include @@ -38,6 +40,7 @@ with this program. If not, see #include "util.h" #include "obs-utils.hpp" #include "setup-wizard.hpp" +#include "api.hpp" const std::map extensionMap{ {".jpg", "/images/"}, {".jpeg", "/images/"}, @@ -45,7 +48,7 @@ const std::map extensionMap{ {".bmp", "/images/"}, {".webm", "/video/"}, {".mov", "/video/"}, {".mp4", "/video/"}, {".mkv", "/video/"}, {".mp3", "/audio/"}, - {".wav", "/aduio/"}, {".effect", "/shaders/"}, + {".wav", "/audio/"}, {".effect", "/shaders/"}, {".shader", "/shaders/"}, {".hlsl", "/shaders/"}, {".lua", "/scripts/"}, {".py", "/scripts/"}, {".html", "/browser-sources/"}, @@ -122,7 +125,8 @@ bool SceneBundle::FromElgatoCloudFile(std::string filePath, std::string SceneBundle::ExtractBundleInfo(std::string filePath) { miniz_cpp::zip_file file(filePath); - return file.read("bundle_info.json"); + auto result = file.read("bundle_info.json"); + return result; } void SceneBundle::SceneCollectionCreated(enum obs_frontend_event event, @@ -146,51 +150,270 @@ void SceneBundle::SceneCollectionChanged(enum obs_frontend_event event, } } } - -bool SceneBundle::ToCollection(std::string collection_name, - std::map videoSettings, - std::string audioSettings) +bool SceneBundle::MergeCollection(std::string collection_name, + std::vector scenes, + std::map videoSettings, + std::string audioSettings, std::string id) { const auto userConf = GetUserConfig(); + _backupCurrentCollection(); + auto curCollectionPath = _currentCollectionPath(); + char* collection_str = os_quick_read_utf8_file(curCollectionPath.c_str()); + nlohmann::json currentCollectionJson = nlohmann::json::parse(collection_str); + bfree(collection_str); + + std::vector cColSourceNames; + std::vector cColUuids; + std::vector cColGroupNames; + std::map cVideoCaptureSources; + + // Grab all source names and UUIDs in the existing collection so that + // we can make sure there are no name/id clashes. UUIDs might clash + // if the same scene collection is merged twice. + if (currentCollectionJson.contains("sources")) { + for (auto source : currentCollectionJson["sources"]) { + cColSourceNames.push_back(source["name"]); + cColUuids.push_back(source["uuid"]); + if (source.contains("filters")) { + for (auto filter : source["filters"]) { + cColUuids.push_back(filter["uuid"]); + } + } - std::string curCollectionName = - config_get_string(userConf, "Basic", "SceneCollection"); - std::string curCollectionFileName = - get_current_scene_collection_filename(); - curCollectionFileName = - get_scene_collections_path() + curCollectionFileName; + // TODO: Mac Compatibility + if (source["id"] == "dshow_input") { + std::string vdi = source["settings"]["video_device_id"]; + cVideoCaptureSources[vdi] = source; + } + } + } - char *ccpath = os_get_abs_path_ptr(curCollectionFileName.c_str()); - std::string curCollectionPath = std::string(ccpath); + if (currentCollectionJson.contains("groups")) { + for (auto group : currentCollectionJson["groups"]) { + cColSourceNames.push_back(group["name"]); + cColUuids.push_back(group["uuid"]); + if (group.contains("filters")) { + for (auto filter : group["filters"]) { + cColUuids.push_back(filter["uuid"]); + } + } + } + } - size_t pos = 0; - std::string from = "\\"; - std::string to = "/"; - while ((pos = curCollectionPath.find(from, pos)) != std::string::npos) { - curCollectionPath.replace(pos, from.length(), to); - pos += to.length(); + if (currentCollectionJson.contains("transitions")) { + for (auto transition : currentCollectionJson["transitions"]) { + cColSourceNames.push_back(transition["name"]); + } } - bfree(ccpath); + std::string collection_file_path = _packPath + "/collection.json"; + char* ecollection_str = + os_quick_read_utf8_file(collection_file_path.c_str()); + std::string collectionData = ecollection_str; + bfree(ecollection_str); + + std::string bundle_info_path = _packPath + "/bundle_info.json"; + char* bundle_info_str = os_quick_read_utf8_file(bundle_info_path.c_str()); + std::string bundleInfoData = bundle_info_str; + bfree(bundle_info_str); + + // replace all uuid clashses with a new uuid + for (auto uuid : cColUuids) { + auto newUuid = gen_uuid(); + replace_all(collectionData, uuid, newUuid); + } - std::string savePath = QDir::homePath().toStdString(); - savePath += "/AppData/Local/Elgato/MarketplaceConnect/SCBackups/"; - os_mkdirs(savePath.c_str()); + // Replace all name clashes in the new scene collection (names that + // exist in the current collection) + for (auto name : cColSourceNames) { + int i = 2; + std::string newName = name + "_" + std::to_string(i); + while ( + std::find(cColSourceNames.begin(), cColSourceNames.end(), newName) != cColSourceNames.end() + || collectionData.find(std::string("\"" + newName + "\"")) != std::string::npos + ) { + i += 1; + newName = name + "_" + std::to_string(i); + } + for (auto& sceneName : scenes) { + if (sceneName == name) { + sceneName = newName; + } + } + replace_all(collectionData, "\"" + name + "\"", "\"" + newName + "\""); + } - std::string backupFilename = get_current_scene_collection_filename(); - savePath += backupFilename; + std::string needle = "{FILE}:"; + std::string word = _packPath + "/"; + replace_all(collectionData, needle, word); - if (os_file_exists(savePath.c_str())) { - os_unlink(savePath.c_str()); + for (auto const& [sourceName, settings] : videoSettings) { + needle = "\"{" + sourceName + "}\""; + replace_all(collectionData, needle, settings); + } + + needle = "\"{AUDIO_CAPTURE_SETTINGS}\""; + replace_all(collectionData, needle, audioSettings); + + _collection = nlohmann::json::parse(collectionData); + + if (_collection.contains("modules") && + _collection["modules"].contains("elgato_marketplace_connect") && + _collection["modules"]["elgato_marketplace_connect"].contains("id") + ) { // This was a downloaded collection that has been exported + auto api = elgatocloud::MarketplaceApi::getInstance(); + std::string currentId = api->id(); + std::string embeddedId = _collection["modules"]["elgato_marketplace_connect"]["id"]; + if (currentId != embeddedId) { + obs_log(LOG_INFO, "Ids don't match"); + QMessageBox msgBox; + msgBox.setWindowTitle("Alert"); + msgBox.setText("This scene collection file contains portions of a scene collection purchased on the Elgato Marketplace. To install it, you will need to be logged in to the original account that purchased the collection. Please log in through the Elgato Marketplace menu item, and try to install again."); + msgBox.setIcon(QMessageBox::Information); + msgBox.setStandardButtons(QMessageBox::Close); + msgBox.exec(); + return false; + } + id = embeddedId; + } + + // Code to swap out video capture devices in _collection, + // with source clones if needed. + + for (auto& source : _collection["sources"]) { + if (source["id"] == "dshow_input") { + // If the user did not select a video device for this + // source.. continue + if (!source["settings"].contains("video_device_id")) { + continue; + } + std::string vdi = source["settings"]["video_device_id"]; + if (cVideoCaptureSources.find(vdi) != cVideoCaptureSources.end()) { + source["id"] = "source-clone"; + source["versioned_id"] = "source-clone"; + source["settings"] = { + {"clone", cVideoCaptureSources[vdi]["name"]}, + {"audio", false}, + {"active_clone", false} + }; + } + } + } + + std::vector mergeSources = {}; + std::vector mergeGroups = {}; + std::vector mergeSceneOrder = {}; + if (scenes.size() == 0) { + for (auto& source : _collection["sources"]) { + mergeSources.push_back(source); + } + for (auto& group : _collection["groups"]) { + mergeGroups.push_back(group); + } + for (auto& so : _collection["scene_order"]) { + mergeSceneOrder.push_back(so); + } + } else { + // 1. Get all source names in collection + std::map sourceNames; + for (auto& source : _collection["sources"]) { + sourceNames[source["name"]] = source; + } + + std::map groupNames; + for (auto& group : _collection["groups"]) { + groupNames[group["name"]] = group; + } + + // 2. Determine names of all required sources + std::set requiredSources; + std::set requiredGroups; + + for (auto& sceneName : scenes) { + addSources( + sceneName, + requiredSources, + requiredGroups, + sourceNames, + groupNames + ); + } + + // 3. Get scene order + std::vector colSceneOrder = _collection["scene_order"]; + std::copy_if(colSceneOrder.begin(), colSceneOrder.end(), std::back_inserter(mergeSceneOrder), + [requiredSources](nlohmann::json const& scene) { + return requiredSources.find(scene["name"]) != requiredSources.end(); + }); + + // 4. Collect required sources + for (auto sourceName : requiredSources) { + mergeSources.push_back(sourceNames[sourceName]); + } + + for (auto groupName : requiredGroups) { + mergeGroups.push_back(groupNames[groupName]); + } + } + + for (auto& source : mergeSources) { + currentCollectionJson["sources"].push_back(source); + } + + for (auto& group : mergeGroups) { + currentCollectionJson["groups"].push_back(group); + } + + for (auto& transition : _collection["transitions"]) { + currentCollectionJson["transitions"].push_back(transition); + } + + currentCollectionJson["current_transition"] = _collection["current_transition"]; + currentCollectionJson["transition_duration"] = _collection["transition_duration"]; + + for (auto& so : currentCollectionJson["scene_order"]) { + mergeSceneOrder.push_back(so); } - os_copyfile(curCollectionPath.c_str(), savePath.c_str()); - char *current_collection = obs_frontend_get_current_scene_collection(); + currentCollectionJson["scene_order"] = mergeSceneOrder; + + _bundleInfo = nlohmann::json::parse(bundleInfoData); + + nlohmann::json module_info = { + {"first_run", true} + }; + + if (_bundleInfo.contains("third_party")) { + module_info["third_party"] = _bundleInfo["third_party"]; + } + + if (!id.empty()) { + module_info["id"] = id; + } + + _collection = currentCollectionJson; + + _collection["modules"]["elgato_marketplace_connect"] = module_info; + + return _createSceneCollection(collection_name); +} + +bool SceneBundle::ToCollection(std::string collection_name, + std::map videoSettings, + std::string audioSettings, std::string id) +{ + const auto userConf = GetUserConfig(); + _backupCurrentCollection(); + std::string collection_file_path = _packPath + "/collection.json"; + std::string bundle_info_path = _packPath + "/bundle_info.json"; char *collection_str = os_quick_read_utf8_file(collection_file_path.c_str()); + char* bundle_info_str = os_quick_read_utf8_file(bundle_info_path.c_str()); std::string collectionData = collection_str; + std::string bundleInfoData = bundle_info_str; bfree(collection_str); + bfree(bundle_info_str); std::string needle = "{FILE}:"; std::string word = _packPath + "/"; @@ -205,14 +428,54 @@ bool SceneBundle::ToCollection(std::string collection_name, replace_all(collectionData, needle, audioSettings); _collection = nlohmann::json::parse(collectionData); - _collection["name"] = collection_name; + _bundleInfo = nlohmann::json::parse(bundleInfoData); + + if (_collection.contains("modules") && + _collection["modules"].contains("elgato_marketplace_connect") && + _collection["modules"]["elgato_marketplace_connect"].contains("id") + ) { // This was a downloaded collection that has been exported + auto api = elgatocloud::MarketplaceApi::getInstance(); + std::string currentId = api->id(); + std::string embeddedId = _collection["modules"]["elgato_marketplace_connect"]["id"]; + if (currentId != embeddedId) { + obs_log(LOG_INFO, "Ids don't match"); + QMessageBox msgBox; + msgBox.setWindowTitle("Alert"); + msgBox.setText("This scene collection file contains portions of a scene collection purchased on the Elgato Marketplace. To install it, you will need to be logged in to the original account that purchased the collection. Please log in through the Elgato Marketplace menu item, and try to install again."); + msgBox.setIcon(QMessageBox::Information); + msgBox.setStandardButtons(QMessageBox::Close); + msgBox.exec(); + return false; + } + id = embeddedId; + } + + nlohmann::json module_info = { + {"first_run", true} + }; + + if (_bundleInfo.contains("third_party")) { + module_info["third_party"] = _bundleInfo["third_party"]; + } + + if (!id.empty()) { + module_info["id"] = id; + } - // TODO: Replacements for OS-specific interfaces, or make built-in OBS - // importers work. Built-in OBS importers would require json11 + _collection["modules"]["elgato_marketplace_connect"] = module_info; + + return _createSceneCollection(collection_name); +} + +bool SceneBundle::_createSceneCollection(std::string collection_name) +{ + char* current_collection = obs_frontend_get_current_scene_collection(); + const auto userConf = GetUserConfig(); + _collection["name"] = collection_name; // 1. Add callback listener for scene collection list changed event obs_frontend_add_event_callback(SceneBundle::SceneCollectionCreated, - this); + this); // 2. Set waiting to true, to block execution until scene collection created. _waiting = true; @@ -225,7 +488,7 @@ bool SceneBundle::ToCollection(std::string collection_name, // 5. Remove the callback obs_frontend_remove_event_callback(SceneBundle::SceneCollectionCreated, - this); + this); // 6. Get new collection name and filename std::string newCollectionName = @@ -243,7 +506,7 @@ bool SceneBundle::ToCollection(std::string collection_name, // 8. Add a callback for scene collection change obs_frontend_add_event_callback(SceneBundle::SceneCollectionChanged, - this); + this); // 9. Switch back to old scene collection so we can manually write the new // collection json file @@ -254,7 +517,7 @@ bool SceneBundle::ToCollection(std::string collection_name, ; // do nothing // 11. Replace newCollectionFileName with imported json data - obs_data_t *data = + obs_data_t* data = obs_data_create_from_json(_collection.dump().c_str()); bool success = obs_data_save_json_safe( data, newCollectionFileName.c_str(), "tmp", "bak"); @@ -284,8 +547,53 @@ bool SceneBundle::ToCollection(std::string collection_name, return true; } +void SceneBundle::_backupCurrentCollection() +{ + auto curCollectionPath = _currentCollectionPath(); + + std::string savePath = QDir::homePath().toStdString(); + savePath += "/AppData/Local/Elgato/MarketplaceConnect/SCBackups/"; + os_mkdirs(savePath.c_str()); + + std::string backupFilename = get_current_scene_collection_filename(); + savePath += backupFilename; + + if (os_file_exists(savePath.c_str())) { + os_unlink(savePath.c_str()); + } + os_copyfile(curCollectionPath.c_str(), savePath.c_str()); +} + +std::string SceneBundle::_currentCollectionPath() +{ + const auto userConf = GetUserConfig(); + + std::string curCollectionName = + config_get_string(userConf, "Basic", "SceneCollection"); + std::string curCollectionFileName = + get_current_scene_collection_filename(); + curCollectionFileName = + get_scene_collections_path() + curCollectionFileName; + + char* ccpath = os_get_abs_path_ptr(curCollectionFileName.c_str()); + std::string curCollectionPath = std::string(ccpath); + + size_t pos = 0; + std::string from = "\\"; + std::string to = "/"; + while ((pos = curCollectionPath.find(from, pos)) != std::string::npos) { + curCollectionPath.replace(pos, from.length(), to); + pos += to.length(); + } + + bfree(ccpath); + return curCollectionPath; +} + SceneBundleStatus SceneBundle::ToElgatoCloudFile( std::string file_path, std::vector plugins, + std::vector> thirdParty, + std::vector outputScenes, std::map videoDeviceDescriptions) { _interrupt = false; @@ -296,6 +604,24 @@ SceneBundleStatus SceneBundle::ToElgatoCloudFile( struct obs_video_info ovi = {}; obs_get_video_info(&ovi); + std::vector> oScenes; + for (auto const& scene : outputScenes) { + std::map s = { + {"id", scene.id }, + {"name", scene.name } + }; + oScenes.push_back(s); + } + + std::vector> thirdPartyReqs; + for (auto const& req : thirdParty) { + std::map r = { + {"name", req.first}, + {"url", req.second} + }; + thirdPartyReqs.push_back(r); + } + nlohmann::json bundleInfo; bundleInfo["canvas"]["width"] = ovi.base_width; bundleInfo["canvas"]["height"] = ovi.base_height; @@ -303,7 +629,9 @@ SceneBundleStatus SceneBundle::ToElgatoCloudFile( bundleInfo["ec_version"] = "1.0"; bundleInfo["id"] = gen_uuid(); bundleInfo["plugins_required"] = plugins; + bundleInfo["third_party"] = thirdPartyReqs; bundleInfo["video_devices"] = videoDeviceDescriptions; + bundleInfo["output_scenes"] = oScenes; // Write the scene collection json file to zip archive. std::string collection_json = _collection.dump(2); @@ -659,3 +987,47 @@ void SceneBundle::_reset() _skippedFilters.clear(); _videoCaptureDevices.clear(); } + + +void addSources( + std::string sourceName, + std::set& requiredSources, + std::set& requiredGroups, + std::map& sourceNames, + std::map& groupNames +) +{ + bool isSource = sourceNames.find(sourceName) != sourceNames.end(); + bool isGroup = groupNames.find(sourceName) != groupNames.end(); + + // Return if the source isn't in the json file. This should + // actually throw an error. + if (!isSource && !isGroup) { + return; + } + + // Return if we've already added this source. + if (isSource && requiredSources.find(sourceName) != requiredSources.end()) { + return; + } + + if (isGroup && requiredGroups.find(sourceName) != requiredGroups.end()) { + return; + } + + if (isSource) + requiredSources.insert(sourceName); + else + requiredGroups.insert(sourceName); + + auto source = isSource ? sourceNames[sourceName].flatten() : groupNames[sourceName].flatten(); + + for (auto field : source) { + if ( + field.is_string() && + (sourceNames.find(field) != sourceNames.end() || groupNames.find(field) != groupNames.end()) + ) { + addSources(field, requiredSources, requiredGroups, sourceNames, groupNames); + } + } +} \ No newline at end of file diff --git a/src/scene-bundle.hpp b/src/scene-bundle.hpp index 45a29f7..ae50941 100644 --- a/src/scene-bundle.hpp +++ b/src/scene-bundle.hpp @@ -41,6 +41,17 @@ enum class SceneBundleStatus { Error }; +struct SceneInfo { + std::string name; + std::string id; + bool outputScene; +}; + +struct ThirdPartyRequirement { + std::string name; + std::string url; +}; + class SceneBundle { private: // Key: original file path, Value: new file name @@ -64,9 +75,15 @@ class SceneBundle { std::string destination); bool ToCollection(std::string collection_name, std::map videoSettings, - std::string audioSettings); + std::string audioSettings, std::string id); + bool MergeCollection(std::string collection_name, + std::vector scenes, + std::map videoSettings, + std::string audioSettings, std::string id); SceneBundleStatus ToElgatoCloudFile( std::string file_path, std::vector plugins, + std::vector> thirdParty, + std::vector outputScenes, std::map videoDeviceDescriptions); bool FileCheckDialog(); @@ -95,7 +112,13 @@ class SceneBundle { miniz_cpp::zip_file &ecFile); bool _AddBrowserSourceContentsToZip(std::string dirPath, std::string zipDir, miniz_cpp::zip_file& ecFile); + bool _createSceneCollection(std::string collectionName); void _reset(); + void _backupCurrentCollection(); + + std::string _currentCollectionPath(); }; +void addSources(std::string sourceName, std::set& requiredSources, std::set& requiredGroups, std::map& sourceNames, std::map& groupNames); + #endif // SCENEBUNDLE_H diff --git a/src/scene-collection-info.cpp b/src/scene-collection-info.cpp new file mode 100644 index 0000000..ee11dd5 --- /dev/null +++ b/src/scene-collection-info.cpp @@ -0,0 +1,106 @@ +/* +Elgato Deep-Linking OBS Plug-In +Copyright (C) 2024 Corsair Memory Inc. oss.elgato@corsair.com + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program. If not, see +*/ + +#include "scene-collection-info.hpp" +#include "elgato-styles.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace elgatocloud { + +ThirdPartyItem::ThirdPartyItem(std::string label, + std::string url, QWidget* parent) + : QWidget(parent) +{ + auto layout = new QHBoxLayout(this); + auto itemLabel = new QLabel(this); + setStyleSheet("background-color: #232323;"); + + itemLabel->setText(label.c_str()); + itemLabel->setStyleSheet(EWizardFieldLabel); + layout->addWidget(itemLabel); + + auto spacer = new QWidget(this); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + layout->addWidget(spacer); + + auto downloadButton = new QPushButton(this); + downloadButton->setText(obs_module_text("SetupWizard.MissingPlugins.DownloadButton")); + downloadButton->setStyleSheet(EWizardButtonStyle); + + connect(downloadButton, &QPushButton::released, this, + [url]() { QDesktopServices::openUrl(QUrl(url.c_str())); }); + layout->addWidget(downloadButton); +} + +SceneCollectionInfo::SceneCollectionInfo(std::vector const& rows, QWidget* parent) + : QDialog(parent) +{ + setWindowTitle(obs_module_text("SceneCollectionInfo.WindowTitle")); + setStyleSheet("background-color: #151515;"); + setModal(true); + setFixedSize(800, 448); + QVBoxLayout* layout = new QVBoxLayout(this); + + QLabel* title = new QLabel(this); + title->setText(obs_module_text("SceneCollectionInfo.Title")); + title->setStyleSheet(EWizardStepTitle); + layout->addWidget(title); + + QLabel* subTitle = new QLabel(this); + subTitle->setText(obs_module_text("SceneCollectionInfo.Text")); + subTitle->setWordWrap(true); + subTitle->setStyleSheet(EWizardStepSubTitle); + layout->addWidget(subTitle); + + auto thirdPartyList = new QListWidget(this); + for (auto& row : rows) { + auto item = new QListWidgetItem(); + item->setSizeHint(QSize(0, 60)); + auto itemWidget = new ThirdPartyItem( + row.name.c_str(), row.url.c_str(), this); + thirdPartyList->addItem(item); + thirdPartyList->setItemWidget(item, itemWidget); + } + + thirdPartyList->setStyleSheet(EMissingPluginsStyle); + thirdPartyList->setSpacing(4); + thirdPartyList->setSelectionMode(QAbstractItemView::NoSelection); + thirdPartyList->setFocusPolicy(Qt::FocusPolicy::NoFocus); + + layout->addWidget(thirdPartyList); + layout->addStretch(); + + QHBoxLayout* buttons = new QHBoxLayout(); + QPushButton* closeButton = new QPushButton(this); + closeButton->setText(obs_module_text("SceneCollectionInfo.CloseButton")); + closeButton->setStyleSheet(EWizardButtonStyle); + + connect(closeButton, &QPushButton::released, this, + [this]() { close(); }); + buttons->addStretch(); + buttons->addWidget(closeButton); + layout->addLayout(buttons); +} + +} \ No newline at end of file diff --git a/src/scene-collection-info.hpp b/src/scene-collection-info.hpp new file mode 100644 index 0000000..7f1525d --- /dev/null +++ b/src/scene-collection-info.hpp @@ -0,0 +1,43 @@ +/* +Elgato Deep-Linking OBS Plug-In +Copyright (C) 2024 Corsair Memory Inc. oss.elgato@corsair.com + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program. If not, see +*/ + +#pragma once +#include + +namespace elgatocloud { + +struct SceneCollectionLineItem { + std::string name; + std::string url; +}; + +class ThirdPartyItem : public QWidget { + Q_OBJECT +public: + ThirdPartyItem(std::string label, std::string url, QWidget* parent); +}; + + +class SceneCollectionInfo : public QDialog { + Q_OBJECT + +public: + SceneCollectionInfo(std::vector const& rows, QWidget* parent = nullptr); +}; + +} \ No newline at end of file diff --git a/src/setup-wizard.cpp b/src/setup-wizard.cpp index cbe3f0c..3077699 100644 --- a/src/setup-wizard.cpp +++ b/src/setup-wizard.cpp @@ -42,10 +42,12 @@ with this program. If not, see #include #include "setup-wizard.hpp" #include "elgato-product.hpp" +#include "elgato-cloud-window.hpp" #include "scene-bundle.hpp" #include "obs-utils.hpp" #include "util.h" #include "platform.h" +#include "api.hpp" namespace elgatocloud { @@ -69,56 +71,37 @@ std::string GetBundleInfo(std::string filename) return data; } -StreamPackageHeader::StreamPackageHeader(QWidget *parent, std::string name, - std::string thumbnailPath = "") + +StepsSideBar::StepsSideBar(std::vector const& steps, std::string name, std::string thumbnailPath, QWidget* parent) : QWidget(parent) { - QVBoxLayout *layout = new QVBoxLayout(this); + std::string nameText = obs_module_text("SetupWizard.ImportTitlePrefix"); + nameText += " " + name; + + QVBoxLayout* layout = new QVBoxLayout(this); + auto header = new StreamPackageHeader(this, nameText, thumbnailPath); + + _stepper = new Stepper(steps, this); layout->setSpacing(16); - layout->setContentsMargins(0, 0, 0, 0); - QLabel *nameLabel = new QLabel(this); - nameLabel->setText(name.c_str()); - nameLabel->setSizePolicy(QSizePolicy::Expanding, - QSizePolicy::Preferred); - nameLabel->setStyleSheet("QLabel {font-size: 18pt;}"); - nameLabel->setWordWrap(true); - layout->addWidget(nameLabel); - if (thumbnailPath != "") { - auto thumbnail = _thumbnail(thumbnailPath); - thumbnail->setAlignment(Qt::AlignCenter); - layout->addWidget(thumbnail); - } + layout->setContentsMargins(8, 0, 24, 0); + layout->addWidget(header); + layout->addWidget(_stepper); + layout->addStretch(); +} + +void StepsSideBar::setStep(int step) +{ + _stepper->setStep(step); +} + +void StepsSideBar::incrementStep() +{ + _stepper->incrementStep(); } -QLabel *StreamPackageHeader::_thumbnail(std::string thumbnailPath) +void StepsSideBar::decrementStep() { - int targetHeight = 140; - int cornerRadius = 8; - - QPixmap img; - img.load(thumbnailPath.c_str()); - - int width = img.width(); - int height = img.height(); - int targetWidth = - int((double)targetHeight * (double)width / (double)height); - QPixmap target(targetWidth, targetHeight); - target.fill(Qt::transparent); - QPainter painter(&target); - - painter.setRenderHint(QPainter::Antialiasing, true); - painter.setRenderHint(QPainter::SmoothPixmapTransform, true); - - QPainterPath path = QPainterPath(); - path.addRoundedRect(0, 0, targetWidth, targetHeight, cornerRadius, - cornerRadius); - painter.setClipPath(path); - painter.drawPixmap(0, 0, img.scaledToHeight(targetHeight)); - - QLabel *labelImg = new QLabel(this); - labelImg->setPixmap(target); - labelImg->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); - return labelImg; + _stepper->decrementStep(); } MissingPlugins::MissingPlugins(QWidget *parent, std::string name, @@ -127,19 +110,23 @@ MissingPlugins::MissingPlugins(QWidget *parent, std::string name, : QWidget(parent) { QVBoxLayout *layout = new QVBoxLayout(this); - StreamPackageHeader *header = new StreamPackageHeader(parent, name); - layout->addWidget(header); QLabel *title = new QLabel(this); - title->setText(obs_module_text("SetupWizard.MissingPlugins.Title")); - title->setAlignment(Qt::AlignCenter); - title->setStyleSheet("QLabel {font-size: 14pt;}"); + int count = (int)missing.size(); + if (count > 1) { + std::string titleText = obs_module_text("SetupWizard.MissingPlugins.Title.Plural"); + replace_all(titleText, "{COUNT}", std::to_string(count)); + title->setText(titleText.c_str()); + } else { + title->setText(obs_module_text("SetupWizard.MissingPlugins.Title.Single")); + } + + title->setStyleSheet(EWizardStepTitle); layout->addWidget(title); QLabel *subTitle = new QLabel(this); subTitle->setText(obs_module_text("SetupWizard.MissingPlugins.Text")); - subTitle->setAlignment(Qt::AlignCenter); subTitle->setWordWrap(true); - subTitle->setStyleSheet("QLabel {font-size: 12pt;}"); + subTitle->setStyleSheet(EWizardStepSubTitle); layout->addWidget(subTitle); auto pluginList = new QListWidget(this); @@ -166,8 +153,9 @@ MissingPluginItem::MissingPluginItem(QWidget *parent, std::string label, { auto layout = new QHBoxLayout(this); auto itemLabel = new QLabel(this); + setStyleSheet("background-color: #232323;"); itemLabel->setText(label.c_str()); - itemLabel->setStyleSheet("QLabel {font-size: 12pt;}"); + itemLabel->setStyleSheet(EWizardFieldLabel); layout->addWidget(itemLabel); auto spacer = new QWidget(this); @@ -176,13 +164,64 @@ MissingPluginItem::MissingPluginItem(QWidget *parent, std::string label, auto downloadButton = new QPushButton(this); downloadButton->setText(obs_module_text("SetupWizard.MissingPlugins.DownloadButton")); - downloadButton->setStyleSheet(EPushButtonStyle); + downloadButton->setStyleSheet(EWizardButtonStyle); connect(downloadButton, &QPushButton::released, this, [url]() { QDesktopServices::openUrl(QUrl(url.c_str())); }); layout->addWidget(downloadButton); } +MissingSourceClone::MissingSourceClone(std::string name, std::string thumbnailPath, QWidget* parent) + : QWidget(parent) +{ + auto layout = new QVBoxLayout(this); + layout->setSpacing(16); + layout->addStretch(); + + QLabel* title = new QLabel(this); + title->setText(obs_module_text("SetupWizard.MissingSourceClone.Title")); + title->setStyleSheet(EWizardStepTitle); + title->setAlignment(Qt::AlignCenter); + title->setFixedWidth(320); + title->setWordWrap(true); + auto titleLayout = centeredWidgetLayout(title); + layout->addLayout(titleLayout); + + auto subTitle = new QLabel(obs_module_text("SetupWizard.MissingSourceClone.Description"), this); + subTitle->setStyleSheet(EWizardStepSubTitle); + subTitle->setFixedWidth(320); + subTitle->setAlignment(Qt::AlignCenter); + subTitle->setWordWrap(true); + + auto subTitleLayout = centeredWidgetLayout(subTitle); + layout->addLayout(subTitleLayout); + + // Setup New and Existing buttons. + QHBoxLayout* buttons = new QHBoxLayout(this); + QPushButton* backButton = new QPushButton(this); + backButton->setText(obs_module_text("SetupWizard.BackButton")); + backButton->setStyleSheet(EWizardQuietButtonStyle); + + connect(backButton, &QPushButton::released, this, + [this]() { emit backPressed(); }); + + QPushButton* getButton = new QPushButton(this); + std::string sourceCloneUrl = "https://obsproject.com/forum/resources/source-clone.1632/"; + getButton->setText(obs_module_text("SetupWizard.MissingPlugins.DownloadButton")); + getButton->setStyleSheet(EWizardButtonStyle); + connect(getButton, &QPushButton::released, this, + [sourceCloneUrl]() { + QDesktopServices::openUrl(QUrl(sourceCloneUrl.c_str())); + }); + buttons->addStretch(); + buttons->addWidget(backButton); + buttons->addWidget(getButton); + buttons->addStretch(); + + layout->addLayout(buttons); + layout->addStretch(); +} + InstallType::InstallType(QWidget *parent, std::string name, std::string thumbnailPath) : QWidget(parent) @@ -222,70 +261,144 @@ StartInstall::StartInstall(QWidget* parent, std::string name, : QWidget(parent) { QVBoxLayout* layout = new QVBoxLayout(this); - StreamPackageHeader* header = - new StreamPackageHeader(parent, name, thumbnailPath); - layout->addWidget(header); + layout->setSpacing(16); layout->addStretch(); - QLabel* startInstall = new QLabel(this); - startInstall->setText(obs_module_text("SetupWizard.StartInstall.Title")); - startInstall->setAlignment(Qt::AlignCenter); - startInstall->setStyleSheet("QLabel {font-size: 13pt;}"); - layout->addWidget(startInstall); - // Setup New and Existing buttons. - QHBoxLayout* buttons = new QHBoxLayout(this); - QPushButton* continueButton = new QPushButton(this); - continueButton->setText(obs_module_text("SetupWizard.GetStartedButton")); - continueButton->setStyleSheet(EPushButtonDarkStyle); + if (thumbnailPath != "") { + auto thumbnail = new RoundedImageLabel(8, this); + QPixmap image(thumbnailPath.c_str()); + thumbnail->setImage(image); + thumbnail->setFixedWidth(320); + //thumbnail->setMaximumWidth(320); + thumbnail->setAlignment(Qt::AlignCenter); - connect(continueButton, &QPushButton::released, this, - [this]() { emit continuePressed(); }); - buttons->addWidget(continueButton); - layout->addStretch(); + auto thumbLayout = centeredWidgetLayout(thumbnail); + + layout->addLayout(thumbLayout); + } else { + auto placeholder = new CameraPlaceholder(8, this); + std::string imageBaseDir = GetDataPath(); + imageBaseDir += "/images/"; + std::string imgPath = imageBaseDir + "icon-scene-collection.svg"; + placeholder->setIcon(imgPath.c_str()); + placeholder->setFixedWidth(320); + + auto placeholderLayout = centeredWidgetLayout(placeholder); + + layout->addLayout(placeholderLayout); + } + + std::string nameText = obs_module_text("SetupWizard.ImportTitlePrefix"); + nameText += " " + name; + auto title = new QLabel(nameText.c_str(), this); + title->setStyleSheet(EWizardStepTitle); + title->setAlignment(Qt::AlignCenter); + title->setFixedWidth(320); + title->setWordWrap(true); + + auto titleLayout = centeredWidgetLayout(title); + layout->addLayout(titleLayout); + + auto subTitle = new QLabel(obs_module_text("SetupWizard.StartInstallation.Description"), this); + subTitle->setStyleSheet(EWizardStepSubTitle); + subTitle->setFixedWidth(320); + subTitle->setAlignment(Qt::AlignCenter); + subTitle->setWordWrap(true); + + auto subTitleLayout = centeredWidgetLayout(subTitle); + layout->addLayout(subTitleLayout); + + QHBoxLayout* buttons = new QHBoxLayout(); + QPushButton* newCollectionButton = new QPushButton(this); + newCollectionButton->setText(obs_module_text("SetupWizard.NewCollection")); + newCollectionButton->setStyleSheet(EWizardButtonStyle); + connect(newCollectionButton, &QPushButton::released, this, + [this]() { emit newCollectionPressed(); }); + + QPushButton* mergeCollectionButton = new QPushButton(this); + mergeCollectionButton->setText(obs_module_text("SetupWizard.MergeCollection")); + mergeCollectionButton->setStyleSheet(EWizardQuietButtonStyle); + connect(mergeCollectionButton, &QPushButton::released, this, + [this]() { emit mergeCollectionPressed(); }); + + buttons->addStretch(); + buttons->addWidget(newCollectionButton); + buttons->addWidget(mergeCollectionButton); + buttons->addStretch(); layout->addLayout(buttons); + layout->addStretch(); } -NewCollectionName::NewCollectionName(QWidget *parent, std::string name, - std::string thumbnailPath) +NewCollectionName::NewCollectionName(std::string titleText, std::string subTitleText, std::vector const& steps, int step, std::string name, + std::string thumbnailPath, QWidget* parent) : QWidget(parent) { _existingCollections = GetSceneCollectionNames(); - QVBoxLayout *layout = new QVBoxLayout(this); - StreamPackageHeader *header = - new StreamPackageHeader(parent, name, thumbnailPath); - layout->addWidget(header); - QLabel *nameCollection = new QLabel(this); - nameCollection->setText(obs_module_text("SetupWizard.CreateCollection.Title")); - nameCollection->setStyleSheet( - "QLabel {font-size: 12pt; margin-top: 16px;}"); - layout->addWidget(nameCollection); - + // TODO- check _existingCollections to see if name is in it. + std::string nameValue = name; + auto layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + auto main = new QHBoxLayout(); + main->setAlignment(Qt::AlignTop); + main->setContentsMargins(0, 0, 0, 0); + auto sideBar = new StepsSideBar(steps, name, thumbnailPath, this); + sideBar->setContentsMargins(0, 0, 0, 0); + sideBar->setFixedWidth(240); + sideBar->setStep(step); + auto form = new QVBoxLayout(); + auto title = new QLabel(titleText.c_str(), this); + //auto title = new QLabel(obs_module_text("SetupWizard.CreateCollection.Title"), this); + title->setStyleSheet(EWizardStepTitle); + auto subTitle = new QLabel(subTitleText.c_str(), this); + //auto subTitle = new QLabel(obs_module_text("SetupWizard.CreateCollection.SubTitle"), this); + subTitle->setStyleSheet(EWizardStepSubTitle); + auto image = new RoundedImageLabel(8, this); + std::string imageBaseDir = GetDataPath(); + imageBaseDir += "/images/"; + std::string imgPath = imageBaseDir + "new-sc-example.png"; + QPixmap sampleImage(imgPath.c_str()); + image->setImage(sampleImage); + image->setFixedWidth(364); + image->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + + auto fieldLabel = new QLabel(obs_module_text("SetupWizard.CreateCollection.NewNameLabel")); + fieldLabel->setStyleSheet(EWizardFieldLabel); _nameField = new QLineEdit(this); - _nameField->setPlaceholderText(obs_module_text("SetupWizard.CreateCollection.NewNamePlaceholder")); - _nameField->setStyleSheet(ELineEditStyle); + _nameField->setText(nameValue.c_str()); + _nameField->setStyleSheet(EWizardTextField); _nameField->setMaxLength(64); layout->addWidget(_nameField); connect(_nameField, &QLineEdit::textChanged, [this](const QString text) { _proceedButton->setEnabled(text.length() > 0); }); + form->addWidget(title); + form->addWidget(subTitle); + form->addWidget(image); + form->addWidget(fieldLabel); + form->addWidget(_nameField); + form->addStretch(); + main->addWidget(sideBar); + main->addLayout(form); - QWidget *spacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); - layout->addWidget(spacer); auto buttons = new QHBoxLayout(); - auto hSpacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - buttons->addWidget(hSpacer); + buttons->setContentsMargins(0, 0, 0, 0); + buttons->addStretch(); + + QPushButton* backButton = new QPushButton(this); + backButton->setText(obs_module_text("SetupWizard.BackButton")); + backButton->setStyleSheet(EWizardQuietButtonStyle); + connect(backButton, &QPushButton::released, this, + [this]() { emit backPressed(); }); + _proceedButton = new QPushButton(this); _proceedButton->setText(obs_module_text("SetupWizard.NextButton")); _proceedButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - _proceedButton->setStyleSheet(EPushButtonStyle); - _proceedButton->setDisabled(true); + _proceedButton->setStyleSheet(EWizardButtonStyle); connect(_proceedButton, &QPushButton::released, this, [this]() { - QString qName = _nameField->text(); - std::string name = qName.toUtf8().constData(); - if (std::find(_existingCollections.begin(), + QString qName = _nameField->text(); + std::string name = qName.toUtf8().constData(); + if (std::find(_existingCollections.begin(), _existingCollections.end(), name) != _existingCollections.end()) { QMessageBox::warning( @@ -296,28 +409,31 @@ NewCollectionName::NewCollectionName(QWidget *parent, std::string name, } emit proceedPressed(name.c_str()); }); + buttons->addWidget(backButton); buttons->addWidget(_proceedButton); + + layout->addLayout(main); + layout->addStretch(); layout->addLayout(buttons); } -VideoSetup::VideoSetup(QWidget *parent, std::string name, +VideoSetup::VideoSetup(std::vector const& steps, + int step, + std::string name, std::string thumbnailPath, - std::map videoSourceLabels) + std::map videoSourceLabels, + QWidget* parent) : QWidget(parent) { auto layout = new QVBoxLayout(this); - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - //StreamPackageHeader *header = new StreamPackageHeader(this, name, ""); - //layout->addWidget(header); layout->setContentsMargins(0, 0, 0, 0); - - auto sourceWidget = new QWidget(this); - sourceWidget->setSizePolicy(QSizePolicy::Expanding, - QSizePolicy::Preferred); - sourceWidget->setContentsMargins(0, 0, 0, 0); - auto sourceGrid = new QGridLayout(sourceWidget); - sourceGrid->setContentsMargins(0, 0, 0, 0); - sourceGrid->setSpacing(18); + auto main = new QHBoxLayout(); + main->setAlignment(Qt::AlignTop); + main->setContentsMargins(0, 0, 0, 0); + auto sideBar = new StepsSideBar(steps, name, thumbnailPath, this); + sideBar->setContentsMargins(0, 0, 0, 0); + sideBar->setFixedWidth(240); + sideBar->setStep(step); obs_data_t *config = elgatoCloud->GetConfig(); @@ -329,47 +445,86 @@ VideoSetup::VideoSetup(QWidget *parent, std::string name, ? obs_data_create_from_json(videoSettingsJson.c_str()) : nullptr; - int i = 0; - int j = 0; - bool first = true; - for (auto const &[sourceName, label] : videoSourceLabels) { + auto form = new QVBoxLayout(); + if (videoSourceLabels.size() == 1) { + auto title = new QLabel(obs_module_text("SetupWizard.VideoSetup.Title.Singular"), this); + title->setStyleSheet(EWizardStepTitle); + title->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + auto subTitle = new QLabel(obs_module_text("SetupWizard.VideoSetup.SubTitle.Singular"), this); + subTitle->setStyleSheet(EWizardStepSubTitle); + subTitle->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + form->addWidget(title); + form->addWidget(subTitle); + auto it = videoSourceLabels.begin(); + auto sourceName = it->first; + auto label = it->second; auto vSource = new VideoCaptureSourceSelector( - sourceWidget, label, sourceName, - first ? videoData : nullptr); - sourceGrid->addWidget(vSource, i, j); - i += j % 2; - j += 1; - j %= 2; + this, label, sourceName, videoData); + form->addWidget(vSource); _videoSelectors.push_back(vSource); - first = false; - } + } else if (videoSourceLabels.size() > 1) { + std::string titleText = obs_module_text("SetupWizard.VideoSetup.Title.Plural"); + replace_all(titleText, "{COUNT}", std::to_string(videoSourceLabels.size())); + auto title = new QLabel(titleText.c_str()); + title->setStyleSheet(EWizardStepTitle); + auto subTitle = new QLabel(obs_module_text("SetupWizard.VideoSetup.SubTitle.Plural"), this); + subTitle->setStyleSheet(EWizardStepSubTitle); + form->addWidget(title); + form->addWidget(subTitle); + + auto sourceWidget = new QWidget(this); + sourceWidget->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Preferred); + sourceWidget->setContentsMargins(0, 0, 0, 0); + auto sourceGrid = new QGridLayout(sourceWidget); + sourceGrid->setContentsMargins(0, 0, 0, 0); + sourceGrid->setSpacing(18); - obs_data_release(videoData); + int i = 0; + int j = 0; + bool first = true; + for (auto const &[sourceName, label] : videoSourceLabels) { + auto vSource = new VideoCaptureSourceSelector( + sourceWidget, label, sourceName, + first ? videoData : nullptr); + vSource->setFixedWidth(170); + vSource->setFixedHeight(170); + sourceGrid->addWidget(vSource, i, j); + i += j % 2; + j += 1; + j %= 2; + _videoSelectors.push_back(vSource); + first = false; + } - auto scrollArea = new QScrollArea(this); - scrollArea->setContentsMargins(0, 0, 0, 0); - scrollArea->setWidget(sourceWidget); - scrollArea->setSizePolicy(QSizePolicy::Expanding, - QSizePolicy::Expanding); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea->setStyleSheet("QScrollArea { border: none; }"); - layout->addWidget(scrollArea); + auto scrollArea = new QScrollArea(this); + scrollArea->setContentsMargins(0, 0, 0, 0); + scrollArea->setWidget(sourceWidget); + scrollArea->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Expanding); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setStyleSheet("QScrollArea { border: none; }"); + form->addWidget(scrollArea); + } - auto buttons = new QHBoxLayout(); + form->addStretch(); + + main->addWidget(sideBar); + main->addLayout(form); - auto spacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); QPushButton *backButton = new QPushButton(this); backButton->setText(obs_module_text("SetupWizard.BackButton")); - backButton->setStyleSheet(EPushButtonStyle); + backButton->setStyleSheet(EWizardQuietButtonStyle); QPushButton *proceedButton = new QPushButton(this); proceedButton->setText(obs_module_text("SetupWizard.NextButton")); - proceedButton->setStyleSheet(EPushButtonStyle); + proceedButton->setStyleSheet(EWizardButtonStyle); - buttons->addWidget(spacer); + auto buttons = new QHBoxLayout(); + buttons->setContentsMargins(0, 0, 0, 0); + buttons->addStretch(); buttons->addWidget(backButton); buttons->addWidget(proceedButton); connect(proceedButton, &QPushButton::released, this, [this]() { @@ -385,8 +540,11 @@ VideoSetup::VideoSetup(QWidget *parent, std::string name, connect(backButton, &QPushButton::released, this, [this]() { emit backPressed(); }); + layout->addLayout(main); + layout->addStretch(); layout->addLayout(buttons); obs_data_release(config); + obs_data_release(videoData); } VideoSetup::~VideoSetup() {} @@ -427,31 +585,39 @@ void VideoSetup::OpenConfigVideoSource() obs_properties_destroy(props); } -AudioSetup::AudioSetup(QWidget *parent, std::string name, - std::string thumbnailPath) +AudioSetup::AudioSetup(std::vector const& steps, int step, std::string name, + std::string thumbnailPath, QWidget* parent) : QWidget(parent) { auto layout = new QVBoxLayout(this); - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); layout->setContentsMargins(0, 0, 0, 0); + auto main = new QHBoxLayout(); + main->setAlignment(Qt::AlignTop); + main->setContentsMargins(0, 0, 0, 0); + auto sideBar = new StepsSideBar(steps, name, thumbnailPath, this); + sideBar->setContentsMargins(0, 0, 0, 0); + sideBar->setFixedWidth(240); + sideBar->setStep(step); + + obs_data_t* config = elgatoCloud->GetConfig(); + + auto form = new QVBoxLayout(); QLabel *audioDeviceLabel = new QLabel(this); audioDeviceLabel->setText(obs_module_text("SetupWizard.AudioSetup.Device.Text")); _audioSources = new QComboBox(this); - _audioSources->setStyleSheet(EComboBoxStyle); + _audioSources->setStyleSheet(EWizardComboBoxStyle); _levelsWidget = new SimpleVolumeMeter(this, _volmeter); // Add Dropdown and meter - QWidget *spacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); - layout->addWidget(audioDeviceLabel); - layout->addWidget(_audioSources); - layout->addWidget(_levelsWidget); - layout->addWidget(spacer); + form->addWidget(audioDeviceLabel); + form->addWidget(_audioSources); + form->addWidget(_levelsWidget); + form->addStretch(); - // Get settings - obs_data_t *config = elgatoCloud->GetConfig(); + main->addWidget(sideBar); + main->addLayout(form); std::string audioSettingsJson = obs_data_get_string(config, "DefaultAudioCaptureSettings"); @@ -491,18 +657,15 @@ AudioSetup::AudioSetup(QWidget *parent, std::string name, auto buttons = new QHBoxLayout(); - auto bSpacer = new QWidget(this); - bSpacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - QPushButton *backButton = new QPushButton(this); backButton->setText(obs_module_text("SetupWizard.BackButton")); - backButton->setStyleSheet(EPushButtonStyle); + backButton->setStyleSheet(EWizardQuietButtonStyle); QPushButton *proceedButton = new QPushButton(this); - proceedButton->setText(obs_module_text("SetupWizard.NextButton")); - proceedButton->setStyleSheet(EPushButtonStyle); + proceedButton->setText(obs_module_text("SetupWizard.InstallButton")); + proceedButton->setStyleSheet(EWizardButtonStyle); - buttons->addWidget(bSpacer); + buttons->addStretch(); buttons->addWidget(backButton); buttons->addWidget(proceedButton); connect(proceedButton, &QPushButton::released, this, [this]() { @@ -514,6 +677,9 @@ AudioSetup::AudioSetup(QWidget *parent, std::string name, }); connect(backButton, &QPushButton::released, this, [this]() { emit backPressed(); }); + + layout->addLayout(main); + layout->addStretch(); layout->addLayout(buttons); obs_data_release(config); } @@ -629,37 +795,169 @@ AudioSetup::~AudioSetup() } } +MergeSelectScenes::MergeSelectScenes(std::vector& outputScenes, std::vector const& steps, int step, std::string name, + std::string thumbnailPath, QWidget* parent) + : QWidget(parent), _outputScenes(outputScenes) +{ + std::string imageBaseDir = GetDataPath(); + imageBaseDir += "/images/"; + std::string checkedImage = imageBaseDir + "checkbox_checked.png"; + std::string uncheckedImage = imageBaseDir + "checkbox_unchecked.png"; + QString checklistStyle = EWizardChecklistStyle; + checklistStyle.replace("${checked-img}", checkedImage.c_str()); + checklistStyle.replace("${unchecked-img}", uncheckedImage.c_str()); + + auto layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + auto main = new QHBoxLayout(); + main->setAlignment(Qt::AlignTop); + main->setContentsMargins(0, 0, 0, 0); + auto sideBar = new StepsSideBar(steps, name, thumbnailPath, this); + sideBar->setContentsMargins(0, 0, 0, 0); + sideBar->setFixedWidth(240); + sideBar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); + sideBar->setStep(step); + + auto form = new QVBoxLayout(); + auto title = new QLabel(obs_module_text("SetupWizard.MergeSelectScenes.Title"), this); + title->setStyleSheet(EWizardStepTitle); + auto subTitle = new QLabel(obs_module_text("SetupWizard.MergeSelectScenes.SubTitle"), this); + subTitle->setStyleSheet(EWizardStepSubTitle); + subTitle->setWordWrap(true); + form->addWidget(title); + form->addWidget(subTitle); + + auto allScenes = new QCheckBox( + obs_module_text("SetupWizard.MergeSelectScenes.AllScenes"), this); + allScenes->setChecked(true); + + QString checkBoxStyle = EWizardCheckBoxStyle; + checkBoxStyle.replace("${checked-img}", checkedImage.c_str()); + checkBoxStyle.replace("${unchecked-img}", uncheckedImage.c_str()); + allScenes->setStyleSheet(checkBoxStyle); + allScenes->setToolTip(obs_module_text("SetupWizard.MergeSelectScenes.AllScenes.Tooltip")); + form->addWidget(allScenes); + + if (_outputScenes.size() > 0) { + auto sceneList = new QListWidget(this); + sceneList->setSizePolicy(QSizePolicy::Preferred, + QSizePolicy::Expanding); + sceneList->setSpacing(8); + sceneList->setStyleSheet(checklistStyle); + sceneList->setSelectionMode(QAbstractItemView::NoSelection); + sceneList->setFocusPolicy(Qt::FocusPolicy::NoFocus); + sceneList->setDisabled(true); + + for (auto scene : _outputScenes) { + sceneList->addItem(scene.name.c_str()); + } + + QListWidgetItem* item = nullptr; + for (int i = 0; i < sceneList->count(); ++i) { + item = sceneList->item(i); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(_outputScenes[i].enabled ? Qt::Checked : Qt::Unchecked); + } + + connect(allScenes, &QCheckBox::stateChanged, [sceneList](int state) { + bool all = state == Qt::Checked; + sceneList->setDisabled(state); + }); + + connect(sceneList, &QListWidget::itemChanged, this, + [this, sceneList](QListWidgetItem* item) { + int index = sceneList->row(item); + bool outputScene = item->checkState() == Qt::Checked; + _outputScenes[index].enabled = outputScene; + }); + form->addWidget(sceneList); + } else { + allScenes->setDisabled(true); + auto subTitle = new QLabel(obs_module_text("SetupWizard.MergeSelectScenes.NoCustomMergeAvailable"), this); + subTitle->setStyleSheet(EWizardStepSubTitle); + subTitle->setFixedWidth(320); + subTitle->setAlignment(Qt::AlignCenter); + subTitle->setWordWrap(true); + + auto subTitleLayout = centeredWidgetLayout(subTitle); + form->addStretch(); + form->addLayout(subTitleLayout); + form->addStretch(); + } + + main->addWidget(sideBar); + main->addLayout(form); + + auto buttons = new QHBoxLayout(); + + QPushButton* backButton = new QPushButton(this); + backButton->setText(obs_module_text("SetupWizard.BackButton")); + backButton->setStyleSheet(EWizardQuietButtonStyle); + + QPushButton* proceedButton = new QPushButton(this); + proceedButton->setText(obs_module_text("SetupWizard.NextButton")); + proceedButton->setStyleSheet(EWizardButtonStyle); + + buttons->addStretch(); + buttons->addWidget(backButton); + buttons->addWidget(proceedButton); + connect(proceedButton, &QPushButton::released, this, [this]() { + emit proceedPressed(); + }); + connect(backButton, &QPushButton::released, this, + [this]() { emit backPressed(); }); + + layout->addLayout(main); + layout->addStretch(); + layout->addLayout(buttons); +} + +MergeSelectScenes::~MergeSelectScenes() +{ + +} + +std::vector MergeSelectScenes::getSelectedScenes() +{ + std::vector scenes; + for (auto const& scene : _outputScenes) { + if (scene.enabled) { + scenes.push_back(scene.name); + } + } + return scenes; +} + + Loading::Loading(QWidget *parent) : QWidget(parent) { QVBoxLayout *layout = new QVBoxLayout(this); - auto spacerTop = new QWidget(this); - spacerTop->setSizePolicy(QSizePolicy::Preferred, - QSizePolicy::Expanding); - layout->addWidget(spacerTop); + layout->addStretch(); - auto spinner = new SpinnerPanel(this, "", "", false); - layout->addWidget(spinner); + auto spinner = new SmallSpinner(this); + auto spinnerLayout = centeredWidgetLayout(spinner); + layout->addLayout(spinnerLayout); auto title = new QLabel(this); title->setText(obs_module_text("SetupWizard.Loading.Title")); - title->setStyleSheet(ETitleStyle); + title->setStyleSheet(EBlankSlateTitleStyle); title->setAlignment(Qt::AlignCenter); - layout->addWidget(title); + title->setFixedWidth(360); + auto titleLayout = centeredWidgetLayout(title); + layout->addLayout(titleLayout); auto subTitle = new QLabel(this); subTitle->setText( obs_module_text("SetupWizard.Loading.Text")); - subTitle->setStyleSheet( - "QLabel{ font-size: 13pt; }"); + subTitle->setStyleSheet(EBlankSlateSubTitleStyle); subTitle->setAlignment(Qt::AlignCenter); subTitle->setWordWrap(true); - layout->addWidget(subTitle); + subTitle->setFixedWidth(360); + auto subTitleLayout = centeredWidgetLayout(subTitle); + layout->addLayout(subTitleLayout); - auto spacerBottom = new QWidget(this); - spacerBottom->setSizePolicy(QSizePolicy::Preferred, - QSizePolicy::Expanding); - layout->addWidget(spacerBottom); + layout->addStretch(); layout->setSpacing(16); } @@ -686,7 +984,7 @@ StreamPackageSetupWizard::StreamPackageSetupWizard(QWidget *parent, StreamPackageSetupWizard::~StreamPackageSetupWizard() { if (!_installStarted) { // We've not yet handed control over - // too install routine. + // to install routine. if (_deleteOnClose) { // Delete the scene collection file os_unlink(_filename.c_str()); @@ -696,6 +994,10 @@ StreamPackageSetupWizard::~StreamPackageSetupWizard() EnableVideoCaptureSourcesActive, this); } + auto elgatoCloudWindow = GetElgatoCloudWindow(); + if (elgatoCloudWindow) { + elgatoCloudWindow->resetDownloads(); + } setupWizard = nullptr; } @@ -745,6 +1047,26 @@ void StreamPackageSetupWizard::OpenArchive() std::vector requiredPlugins = bundleInfo ["plugins_required"]; + std::vector outputScenes = {}; + if (bundleInfo.contains("output_scenes")) { + for (auto const& outputScene : bundleInfo["output_scenes"]) { + if (outputScene.contains("name") && outputScene.contains("id")) { + outputScenes.push_back({ + outputScene["id"], + outputScene["name"], + true + }); + } + } + } + + // In case the setup wizard was closed while the + // archive was being extracted, stop execution before + // anything is called on the now null setupWizard pointer. + auto setupWizard = GetSetupWizard(); + if (!setupWizard) { + return; + } // Disable all video capture sources so that single-thread // capture sources, such as the Elgato Facecam, can be properly @@ -764,7 +1086,7 @@ void StreamPackageSetupWizard::OpenArchive() missing); } else { _buildSetupUI( - videoSourceLabels); + videoSourceLabels, outputScenes); } }); }) @@ -823,105 +1145,107 @@ bool StreamPackageSetupWizard::EnableVideoCaptureSourcesActive(void* data, void StreamPackageSetupWizard::_buildMissingPluginsUI( std::vector &missing) { - setFixedSize(500, 350); + setFixedSize(640, 512); //QVBoxLayout *layout = new QVBoxLayout(this); auto missingPlugins = new MissingPlugins(this, _productName, _thumbnailPath, missing); - _steps->addWidget(missingPlugins); - _steps->setCurrentIndex(1); + _container->addWidget(missingPlugins); + _container->setCurrentIndex(1); } void StreamPackageSetupWizard::_buildBaseUI() { - setFixedSize(320, 400); + setFixedSize(640, 448); QVBoxLayout *layout = new QVBoxLayout(this); - _steps = new QStackedWidget(this); + _container = new QStackedWidget(this); auto loading = new Loading(this); - _steps->addWidget(loading); + _container->addWidget(loading); setupWizard = this; setWindowTitle(obs_module_text("SetupWizard.WindowTitle")); - setStyleSheet("background-color: #232323"); + setStyleSheet("background-color: #151515;"); - layout->addWidget(_steps); + layout->addWidget(_container); } void StreamPackageSetupWizard::_buildSetupUI( - std::map &videoSourceLabels) + std::map &videoSourceLabels, std::vector& outputScenes) { + setFixedSize(640, 448); - // Step -1- select a new or existing install (step index: -1) - //InstallType *installerType = - // new InstallType(this, _productName, _thumbnailPath); - //connect(installerType, &InstallType::newCollectionPressed, this, - // [this]() { - // _setup.installType = InstallTypes::NewCollection; - // _steps->setCurrentIndex(2); - // setFixedSize(320, 384); - // }); - //connect(installerType, &InstallType::existingCollectionPressed, this, - // [this]() { - // _setup.installType = InstallTypes::AddToCollection; - // _steps->setCurrentIndex(3); - // setFixedSize(320, 384); - // }); - //_steps->addWidget(installerType); - - // Step 1 start install screen StartInstall* startInstall = new StartInstall(this, _productName, _thumbnailPath); - connect(startInstall, &StartInstall::continuePressed, this, + connect(startInstall, &StartInstall::newCollectionPressed, this, [this]() { _setup.installType = InstallTypes::NewCollection; - _steps->setCurrentIndex(2); - setFixedSize(320, 384); + _container->setCurrentIndex(2); + }); + connect(startInstall, &StartInstall::mergeCollectionPressed, this, + [this]() { + _setup.installType = InstallTypes::AddToCollection; + _container->setCurrentIndex(3); }); - _steps->addWidget(startInstall); + _container->addWidget(startInstall); + + _buildNewCollectionUI(videoSourceLabels); + _buildMergeCollectionUI(videoSourceLabels, outputScenes); + + _container->setCurrentIndex(1); +} - // Step 2a- Provide a name for the new collection (step index: 2) +void StreamPackageSetupWizard::_buildNewCollectionUI(std::map& videoSourceLabels) +{ + std::vector steps = { + obs_module_text("SetupWizard.NewCollectionSteps.GetStarted"), + obs_module_text("SetupWizard.NewCollectionSteps.NameSceneCollection"), + obs_module_text("SetupWizard.NewCollectionSteps.SetUpCameras"), + obs_module_text("SetupWizard.NewCollectionSteps.ChooseMicrophone") + }; + + _newCollectionSteps = new QStackedWidget(this); + // Step 1- Provide a name for the new collection (step index: 0) auto *newName = - new NewCollectionName(this, _productName, _thumbnailPath); + new NewCollectionName( + obs_module_text("SetupWizard.CreateCollection.Title"), + obs_module_text("SetupWizard.CreateCollection.SubTitle"), + steps, 1, _productName, _thumbnailPath, this); + connect(newName, &NewCollectionName::proceedPressed, this, [this, videoSourceLabels](std::string name) { _setup.collectionName = name; if (videoSourceLabels.size() > 0) { - _steps->setCurrentIndex(4); - setFixedSize(554, 440); + _newCollectionSteps->setCurrentIndex(1); _vSetup->EnableTempSources(); } else { - _steps->setCurrentIndex(5); - setFixedSize(320, 384); + _newCollectionSteps->setCurrentIndex(2); } }); - _steps->addWidget(newName); - - // Step 2b- Select an existing collection (step index: 3) - _steps->addWidget(new QWidget(this)); + connect(newName, &NewCollectionName::backPressed, this, [this]() { + _container->setCurrentIndex(1); + }); + _newCollectionSteps->addWidget(newName); - // Step 3- Set up Video inputs (step index: 4) - _vSetup = new VideoSetup(this, _productName, _thumbnailPath, - videoSourceLabels); + // Step 2- Set up Video inputs (step index: 1) + _vSetup = new VideoSetup(steps, 2, _productName, _thumbnailPath, + videoSourceLabels, this); _vSetup->DisableTempSources(); - _steps->addWidget(_vSetup); + _newCollectionSteps->addWidget(_vSetup); connect(_vSetup, &VideoSetup::proceedPressed, this, [this](std::map settings) { - //blog(LOG_INFO, "%s", settings.c_str()); _setup.videoSettings = settings; - _steps->setCurrentIndex(5); - setFixedSize(320, 384); + _newCollectionSteps->setCurrentIndex(2); _vSetup->DisableTempSources(); }); connect(_vSetup, &VideoSetup::backPressed, this, [this]() { - _steps->setCurrentIndex(2); - setFixedSize(320, 384); + _newCollectionSteps->setCurrentIndex(0); _vSetup->DisableTempSources(); }); - // Step 4- Setup Audio Inputs (step index: 5) - auto aSetup = new AudioSetup(this, _productName, _thumbnailPath); - _steps->addWidget(aSetup); + // Step 3- Setup Audio Inputs (step index: 2) + auto aSetup = new AudioSetup(steps, 3, _productName, _thumbnailPath, this); + _newCollectionSteps->addWidget(aSetup); connect(aSetup, &AudioSetup::proceedPressed, this, [this](std::string settings) { _setup.audioSettings = settings; @@ -932,15 +1256,170 @@ void StreamPackageSetupWizard::_buildSetupUI( connect(aSetup, &AudioSetup::backPressed, this, [this, videoSourceLabels]() { if (videoSourceLabels.size() > 0) { - _steps->setCurrentIndex(4); - setFixedSize(554, 440); + _newCollectionSteps->setCurrentIndex(1); _vSetup->EnableTempSources(); } else { - _steps->setCurrentIndex(2); - setFixedSize(320, 384); + _newCollectionSteps->setCurrentIndex(0); } }); - _steps->setCurrentIndex(1); + _newCollectionSteps->setCurrentIndex(0); + _container->addWidget(_newCollectionSteps); +} + +void StreamPackageSetupWizard::_buildMergeCollectionUI(std::map& videoSourceLabels, std::vector& outputScenes) +{ + _mergeCollectionSteps = new QStackedWidget(this); + + PluginInfo pi; + std::vector + missing = pi.missing({ "source-clone.dll" }); + if (missing.size() > 0) { + auto missingSourceClone = new MissingSourceClone(_productName, _thumbnailPath, this); + connect(missingSourceClone, &MissingSourceClone::backPressed, this, [this]() { + _container->setCurrentIndex(1); + }); + + _mergeCollectionSteps->addWidget(missingSourceClone); + _mergeCollectionSteps->setCurrentIndex(0); + _container->addWidget(_mergeCollectionSteps); + return; + } + + std::vector steps = { + obs_module_text("SetupWizard.MergeCollectionSteps.GetStarted"), + obs_module_text("SetupWizard.NewCollectionSteps.NameSceneCollection"), + obs_module_text("SetupWizard.MergeCollectionSteps.SelectScenes"), + obs_module_text("SetupWizard.MergeCollectionSteps.SetUpCameras"), + obs_module_text("SetupWizard.MergeCollectionSteps.ChooseMicrophone") + }; + + // Step 1- Provide a name for the new collection (step index: 0) + auto* newName = + new NewCollectionName( + obs_module_text("SetupWizard.MergeCollectionName.Title"), + obs_module_text("SetupWizard.MergeCollectionName.SubTitle"), + steps, 1, _productName, _thumbnailPath, this); + + connect(newName, &NewCollectionName::proceedPressed, this, + [this, videoSourceLabels](std::string name) { + _setup.collectionName = name; + _mergeCollectionSteps->setCurrentIndex(1); + _vSetup->EnableTempSources(); + }); + connect(newName, &NewCollectionName::backPressed, this, [this]() { + _container->setCurrentIndex(1); + }); + _mergeCollectionSteps->addWidget(newName); + + // Step 1- select scenes to merge (step index: 1) + auto mergeScenes = new MergeSelectScenes(outputScenes, steps, 2, _productName, _thumbnailPath, this); + _mergeCollectionSteps->addWidget(mergeScenes); + connect(mergeScenes, &MergeSelectScenes::proceedPressed, this, + [this, mergeScenes, videoSourceLabels]() { + if (videoSourceLabels.size() > 0) { + _vSetupMerge->EnableTempSources(); + _mergeCollectionSteps->setCurrentIndex(2); + } else { + _mergeCollectionSteps->setCurrentIndex(3); + } + _setup.scenesToMerge = mergeScenes->getSelectedScenes(); + }); + + connect(mergeScenes, &MergeSelectScenes::backPressed, this, + [this]() { + _mergeCollectionSteps->setCurrentIndex(0); + }); + + // Step 2- Set up Video inputs (step index: 2) + _vSetupMerge = new VideoSetup(steps, 3, _productName, _thumbnailPath, + videoSourceLabels, this); + _vSetupMerge->DisableTempSources(); + _mergeCollectionSteps->addWidget(_vSetupMerge); + connect(_vSetupMerge, &VideoSetup::proceedPressed, this, + [this](std::map settings) { + _setup.videoSettings = settings; + _mergeCollectionSteps->setCurrentIndex(3); + _vSetupMerge->DisableTempSources(); + }); + connect(_vSetupMerge, &VideoSetup::backPressed, this, [this]() { + //_container->setCurrentIndex(0); + _mergeCollectionSteps->setCurrentIndex(1); + _vSetupMerge->DisableTempSources(); + }); + + // Step 2- Setup Audio Inputs (step index: 3) + auto aSetup = new AudioSetup(steps, 4, _productName, _thumbnailPath, this); + _mergeCollectionSteps->addWidget(aSetup); + connect(aSetup, &AudioSetup::proceedPressed, this, + [this](std::string settings) { + _setup.audioSettings = settings; + // Nuke the video preview window + _installStarted = true; + mergeStreamPackage(_setup, _filename, _deleteOnClose, _toEnable); + }); + connect(aSetup, &AudioSetup::backPressed, this, + [this, videoSourceLabels]() { + if (videoSourceLabels.size() > 0) { + _mergeCollectionSteps->setCurrentIndex(2); + _vSetupMerge->EnableTempSources(); + } else { + _mergeCollectionSteps->setCurrentIndex(1); + } + }); + _mergeCollectionSteps->setCurrentIndex(0); + _container->addWidget(_mergeCollectionSteps); +} + +void mergeStreamPackage(Setup setup, std::string filename, bool deleteOnClose, std::vector toEnable) +{ + // TODO: Clean up this mess of setting up the pack install path. + obs_data_t* config = elgatoCloud->GetConfig(); + auto curFileName = get_current_scene_collection_filename(); + curFileName = get_scene_collections_path() + curFileName; + + std::string path = obs_data_get_string(config, "InstallLocation"); + os_mkdirs(path.c_str()); + std::string unsafeDirName(setup.collectionName); + std::string safeDirName; + generate_safe_path(unsafeDirName, safeDirName); + std::string bundlePath = path + "/" + safeDirName; + + // TODO: Add dialog with progress indicator. Put unzipping and + // scene collection loading code into new threads to stop + // from blocking. + SceneBundle bundle; + if (!bundle.FromElgatoCloudFile(filename, bundlePath)) { + obs_log(LOG_WARNING, "Elgato Install: Could not install %s", + filename.c_str()); + return; + } + obs_data_release(config); + std::string id = ""; + if (deleteOnClose) { + auto api = MarketplaceApi::getInstance(); + id = api->id(); + } + + bool collectionChanged = bundle.MergeCollection( + setup.collectionName, + setup.scenesToMerge, + setup.videoSettings, + setup.audioSettings, + id); + + if (deleteOnClose) { + // Delete the scene collection file + os_unlink(filename.c_str()); + } + + // Handle resetting sources. + if (!collectionChanged) { + obs_enum_sources( + &EnableVideoCaptureSourcesActive, + &toEnable); + } else { + EnableVideoCaptureSourcesJson(toEnable, curFileName); + } } void installStreamPackage(Setup setup, std::string filename, bool deleteOnClose, std::vector toEnable) @@ -967,11 +1446,17 @@ void installStreamPackage(Setup setup, std::string filename, bool deleteOnClose, return; } obs_data_release(config); + std::string id = ""; + if (deleteOnClose) { + auto api = MarketplaceApi::getInstance(); + id = api->id(); + } bool collectionChanged = bundle.ToCollection( setup.collectionName, setup.videoSettings, - setup.audioSettings); + setup.audioSettings, + id); if (deleteOnClose) { // Delete the scene collection file diff --git a/src/setup-wizard.hpp b/src/setup-wizard.hpp index 55eb9fd..cba630a 100644 --- a/src/setup-wizard.hpp +++ b/src/setup-wizard.hpp @@ -55,21 +55,30 @@ enum class InstallTypes { ReplaceCollection }; +struct OutputScene { + std::string id; + std::string name; + bool enabled; +}; + struct Setup { InstallTypes installType; std::string collectionName; std::map videoSettings; std::string audioSettings; + std::vector scenesToMerge; }; -class StreamPackageHeader : public QWidget { +class StepsSideBar : public QWidget { Q_OBJECT public: - StreamPackageHeader(QWidget *parent, std::string name, - std::string thumbnailPath); + StepsSideBar(std::vector const& steps, std::string name, std::string thumbnailPath, QWidget* parent); + void setStep(int step); + void incrementStep(); + void decrementStep(); private: - QLabel *_thumbnail(std::string thumbnailPath); + Stepper* _stepper; }; class MissingPlugins : public QWidget { @@ -86,6 +95,14 @@ class MissingPluginItem : public QWidget { MissingPluginItem(QWidget *parent, std::string label, std::string url); }; +class MissingSourceClone : public QWidget { + Q_OBJECT +public: + MissingSourceClone(std::string name, std::string thumbnailPath, QWidget* parent=nullptr); +signals: + void backPressed(); +}; + class InstallType : public QWidget { Q_OBJECT public: @@ -102,14 +119,18 @@ class StartInstall : public QWidget { StartInstall(QWidget* parent, std::string name, std::string thumbnailPath); signals: - void continuePressed(); + void newCollectionPressed(); + void mergeCollectionPressed(); }; class NewCollectionName : public QWidget { Q_OBJECT public: - NewCollectionName(QWidget *parent, std::string name, - std::string thumbnailPath); + NewCollectionName( + std::string titleText, std::string subTitleText, + std::vector const& steps, int step, + std::string name, std::string thumbnailPath, + QWidget* parent); private: std::vector _existingCollections; @@ -117,13 +138,14 @@ class NewCollectionName : public QWidget { QPushButton *_proceedButton = nullptr; signals: void proceedPressed(std::string name); + void backPressed(); }; class VideoSetup : public QWidget { Q_OBJECT public: - VideoSetup(QWidget *parent, std::string name, std::string thumbnailPath, - std::map videoSourceLabels); + VideoSetup(std::vector const& steps, int step, std::string name, std::string thumbnailPath, + std::map videoSourceLabels, QWidget* parent); ~VideoSetup(); void OpenConfigVideoSource(); @@ -146,8 +168,8 @@ class VideoSetup : public QWidget { class AudioSetup : public QWidget { Q_OBJECT public: - AudioSetup(QWidget *parent, std::string name, - std::string thumbnailPath); + AudioSetup(std::vector const& steps, int step, std::string name, + std::string thumbnailPath, QWidget* parent); ~AudioSetup(); void SetupVolMeter(); @@ -173,6 +195,22 @@ class AudioSetup : public QWidget { void backPressed(); }; + +class MergeSelectScenes : public QWidget { + Q_OBJECT +public: + MergeSelectScenes(std::vector& outputScenes, std::vector const& steps, int step, std::string name, + std::string thumbnailPath, QWidget* parent); + ~MergeSelectScenes(); + std::vector getSelectedScenes(); +signals: + void proceedPressed(); + void backPressed(); + +private: + std::vector _outputScenes; +}; + class Loading : public QWidget { Q_OBJECT public: @@ -199,22 +237,29 @@ class StreamPackageSetupWizard : public QDialog { void _buildBaseUI(); void _buildMissingPluginsUI(std::vector &missing); void - _buildSetupUI(std::map &videoSourceLabels); + _buildSetupUI(std::map &videoSourceLabels, std::vector &outputScenes); + void _buildNewCollectionUI(std::map& videoSourceLabels); + void _buildMergeCollectionUI(std::map& videoSourceLabels, std::vector& outputScenes); std::string _productName; std::string _thumbnailPath; std::string _filename; std::string _curCollectionFileName; bool _deleteOnClose; bool _installStarted; - QStackedWidget *_steps; + QStackedWidget* _newCollectionSteps; + QStackedWidget* _container; + QStackedWidget* _mergeCollectionSteps; Setup _setup; std::vector _toEnable; VideoSetup *_vSetup; + VideoSetup* _vSetupMerge; + VideoSetup* _vSetupSubMerge; QFuture _future; }; void installStreamPackage(Setup setup, std::string filename, bool deleteOnClose, std::vector toEnable); +void mergeStreamPackage(Setup setup, std::string filename, bool deleteOnClose, std::vector toEnable); bool EnableVideoCaptureSourcesActive(void* data, obs_source_t* source); void EnableVideoCaptureSourcesJson(std::vector sourceIds, std::string curFileName); diff --git a/src/util.cpp b/src/util.cpp index 0823370..79538a3 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -661,3 +661,10 @@ std::string decryptString(std::string input) return decrypted; } +std::string getImagesPath() +{ + std::string path = obs_get_module_data_path(obs_current_module()); + replace_all(path, "\\", "/"); + path += "/images/"; + return path; +} diff --git a/src/util.h b/src/util.h index d04c8c6..81796de 100644 --- a/src/util.h +++ b/src/util.h @@ -22,6 +22,8 @@ with this program. If not, see #include #include #include +#include +#include template static size_t write_data(void *ptr, size_t size, size_t nmemb, void *userdata) @@ -90,3 +92,4 @@ std::string releaseType(); std::string encryptString(std::string input); std::string decryptString(std::string input); +std::string getImagesPath();