diff --git a/lib/i18n/da.i18n.json b/lib/i18n/da.i18n.json index 6253f20b..3b6e80b9 100644 --- a/lib/i18n/da.i18n.json +++ b/lib/i18n/da.i18n.json @@ -211,6 +211,12 @@ "autoPipDescription": "Gå automatisk til billede-i-billede når du forlader appen under afspilning", "matchContentFrameRate": "Match indholdets billedhastighed", "matchContentFrameRateDescription": "Juster skærmens opdateringshastighed til videoindhold for at reducere hakken og spare batteri", + "matchRefreshRate": "Match opdateringshastighed", + "matchRefreshRateDescription": "Skift skærmens opdateringshastighed til at matche videoindhold i fuldskærm", + "matchDynamicRange": "Match dynamisk område", + "matchDynamicRangeDescription": "Aktiver automatisk HDR for HDR-indhold og gendan SDR når afspilleren lukkes", + "displaySwitchDelay": "Forsinkelse ved skærmskift", + "displaySwitchDelayDescription": "Sekunder der ventes efter et skærmskift før afspilning starter", "tunneledPlayback": "Tunneleret afspilning", "tunneledPlaybackDescription": "Brug hardwareaccelereret videotunneling. Deaktiver hvis du ser sort skærm med lyd på HDR-indhold", "requireProfileSelectionOnOpen": "Spørg om profil ved åbning", diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index dc21bc40..6b1e9a05 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -211,6 +211,12 @@ "autoPipDescription": "Automatisch Bild-in-Bild aktivieren, wenn die App während der Wiedergabe verlassen wird", "matchContentFrameRate": "Inhalts-Bildrate anpassen", "matchContentFrameRateDescription": "Bildwiederholfrequenz des Displays an den Videoinhalt anpassen, reduziert Ruckeln und spart Akku", + "matchRefreshRate": "Bildwiederholrate anpassen", + "matchRefreshRateDescription": "Bildwiederholrate des Displays an den Videoinhalt im Vollbildmodus anpassen", + "matchDynamicRange": "Dynamikumfang anpassen", + "matchDynamicRangeDescription": "HDR für HDR-Inhalte automatisch aktivieren und beim Verlassen des Players auf SDR zurücksetzen", + "displaySwitchDelay": "Verzögerung beim Displaywechsel", + "displaySwitchDelayDescription": "Sekunden Wartezeit nach einem Displaywechsel vor Wiedergabebeginn", "tunneledPlayback": "Tunnelwiedergabe", "tunneledPlaybackDescription": "Hardware-beschleunigte Video-Tunnelierung verwenden. Deaktivieren, wenn bei HDR-Inhalten ein schwarzer Bildschirm mit Ton erscheint", "requireProfileSelectionOnOpen": "Profil beim Öffnen abfragen", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 0f2e1f21..37e50cc9 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -211,6 +211,12 @@ "autoPipDescription": "Automatically enter picture-in-picture when leaving the app during playback", "matchContentFrameRate": "Match Content Frame Rate", "matchContentFrameRateDescription": "Adjust display refresh rate to match video content, reducing judder and saving battery", + "matchRefreshRate": "Match Refresh Rate", + "matchRefreshRateDescription": "Switch display refresh rate to match video content when in fullscreen", + "matchDynamicRange": "Match Dynamic Range", + "matchDynamicRangeDescription": "Auto-enable HDR for HDR content and revert to SDR when leaving the player", + "displaySwitchDelay": "Display Switch Delay", + "displaySwitchDelayDescription": "Seconds to wait after a display mode change before starting playback", "tunneledPlayback": "Tunneled Playback", "tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content", "requireProfileSelectionOnOpen": "Ask for profile on app open", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index 41fe910f..d4b48aeb 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -211,6 +211,12 @@ "autoPipDescription": "Activar automáticamente imagen en imagen al salir de la app durante la reproducción", "matchContentFrameRate": "Ajustar frecuencia de actualización", "matchContentFrameRateDescription": "Ajustar la frecuencia de actualización de la pantalla para que coincida con el video, reduciendo tirones y ahorrando batería", + "matchRefreshRate": "Ajustar frecuencia de refresco", + "matchRefreshRateDescription": "Cambiar la frecuencia de refresco de la pantalla para coincidir con el contenido de video en pantalla completa", + "matchDynamicRange": "Ajustar rango dinámico", + "matchDynamicRangeDescription": "Activar HDR automáticamente para contenido HDR y volver a SDR al salir del reproductor", + "displaySwitchDelay": "Retraso de cambio de pantalla", + "displaySwitchDelayDescription": "Segundos de espera después de un cambio de pantalla antes de iniciar la reproducción", "tunneledPlayback": "Reproducción tunelizada", "tunneledPlaybackDescription": "Usar tunelización de video acelerada por hardware. Desactivar si ves una pantalla negra con audio en contenido HDR", "requireProfileSelectionOnOpen": "Pedir perfil al abrir la app", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index e2a0dc5d..b5275092 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -211,6 +211,12 @@ "autoPipDescription": "Activer automatiquement l'image dans l'image en quittant l'application pendant la lecture", "matchContentFrameRate": "Fréquence d'images du contenu correspondant", "matchContentFrameRateDescription": "Ajustez la fréquence de rafraîchissement de l'écran en fonction du contenu vidéo, ce qui réduit les saccades et économise la batterie", + "matchRefreshRate": "Adapter la fréquence de rafraîchissement", + "matchRefreshRateDescription": "Changer la fréquence de rafraîchissement de l'écran pour correspondre au contenu vidéo en plein écran", + "matchDynamicRange": "Adapter la plage dynamique", + "matchDynamicRangeDescription": "Activer automatiquement le HDR pour le contenu HDR et revenir en SDR en quittant le lecteur", + "displaySwitchDelay": "Délai de changement d'affichage", + "displaySwitchDelayDescription": "Secondes d'attente après un changement d'affichage avant de démarrer la lecture", "tunneledPlayback": "Lecture tunnelée", "tunneledPlaybackDescription": "Utiliser le tunnelage vidéo accéléré par matériel. Désactiver si vous voyez un écran noir avec du son sur du contenu HDR", "requireProfileSelectionOnOpen": "Demander le profil à l'ouverture", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 029dda92..5f746a1f 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -211,6 +211,12 @@ "autoPipDescription": "Attiva automaticamente il picture-in-picture quando si esce dall'app durante la riproduzione", "matchContentFrameRate": "Adatta frequenza fotogrammi", "matchContentFrameRateDescription": "Regola la frequenza di aggiornamento del display in base al contenuto video, riducendo i tremolii e risparmiando batteria", + "matchRefreshRate": "Adatta frequenza di aggiornamento", + "matchRefreshRateDescription": "Cambia la frequenza di aggiornamento del display per adattarla al contenuto video a schermo intero", + "matchDynamicRange": "Adatta gamma dinamica", + "matchDynamicRangeDescription": "Attiva automaticamente l'HDR per i contenuti HDR e torna in SDR all'uscita dal lettore", + "displaySwitchDelay": "Ritardo cambio display", + "displaySwitchDelayDescription": "Secondi di attesa dopo un cambio di display prima di avviare la riproduzione", "tunneledPlayback": "Riproduzione tunnelizzata", "tunneledPlaybackDescription": "Usa il tunneling video accelerato dall'hardware. Disattiva se vedi uno schermo nero con audio su contenuti HDR", "requireProfileSelectionOnOpen": "Chiedi profilo all'apertura", diff --git a/lib/i18n/ja.i18n.json b/lib/i18n/ja.i18n.json index 622c59a3..f1fcc645 100644 --- a/lib/i18n/ja.i18n.json +++ b/lib/i18n/ja.i18n.json @@ -211,6 +211,12 @@ "autoPipDescription": "再生中にアプリを離れると自動的にピクチャーインピクチャーに移行", "matchContentFrameRate": "コンテンツのフレームレートに合わせる", "matchContentFrameRateDescription": "動画コンテンツに合わせてディスプレイのリフレッシュレートを調整し、ジャダーを低減しバッテリーを節約", + "matchRefreshRate": "リフレッシュレートを合わせる", + "matchRefreshRateDescription": "フルスクリーン時にディスプレイのリフレッシュレートを動画コンテンツに合わせて切り替える", + "matchDynamicRange": "ダイナミックレンジを合わせる", + "matchDynamicRangeDescription": "HDRコンテンツに対して自動的にHDRを有効にし、プレーヤーを閉じるとSDRに戻す", + "displaySwitchDelay": "ディスプレイ切り替え遅延", + "displaySwitchDelayDescription": "ディスプレイモード変更後、再生を開始するまでの待機秒数", "tunneledPlayback": "トンネル再生", "tunneledPlaybackDescription": "ハードウェアアクセラレーションされたビデオトンネリングを使用。HDRコンテンツで音声のみで画面が黒くなる場合は無効にしてください", "requireProfileSelectionOnOpen": "アプリ起動時にプロフィールを確認", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 2e0bd725..ca4e2b13 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -211,6 +211,12 @@ "autoPipDescription": "재생 중 앱을 나갈 때 자동으로 PIP 모드로 전환", "matchContentFrameRate": "콘텐츠 프레임 레이트 맞춤", "matchContentFrameRateDescription": "비디오 콘텐츠에 맞게 디스플레이 주사율을 조정하여 떨림을 줄이고 배터리를 절약합니다", + "matchRefreshRate": "주사율 맞춤", + "matchRefreshRateDescription": "전체 화면에서 비디오 콘텐츠에 맞게 디스플레이 주사율을 전환합니다", + "matchDynamicRange": "다이나믹 레인지 맞춤", + "matchDynamicRangeDescription": "HDR 콘텐츠에 대해 자동으로 HDR을 활성화하고 플레이어를 나갈 때 SDR로 복원합니다", + "displaySwitchDelay": "디스플레이 전환 지연", + "displaySwitchDelayDescription": "디스플레이 모드 변경 후 재생을 시작하기까지 대기하는 시간(초)", "tunneledPlayback": "터널 재생", "tunneledPlaybackDescription": "하드웨어 가속 비디오 터널링을 사용합니다. HDR 콘텐츠에서 소리만 나고 검은 화면이 보이면 비활성화하세요", "requireProfileSelectionOnOpen": "앱 실행 시 프로필 선택", diff --git a/lib/i18n/nb.i18n.json b/lib/i18n/nb.i18n.json index f674df57..b6ce7383 100644 --- a/lib/i18n/nb.i18n.json +++ b/lib/i18n/nb.i18n.json @@ -211,6 +211,12 @@ "autoPipDescription": "Gå automatisk til bilde-i-bilde når du forlater appen under avspilling", "matchContentFrameRate": "Tilpass innholdets bildefrekvens", "matchContentFrameRateDescription": "Juster skjermens oppdateringsfrekvens for å matche videoinnhold, reduserer hakking og sparer batteri", + "matchRefreshRate": "Tilpass oppdateringsfrekvens", + "matchRefreshRateDescription": "Bytt skjermens oppdateringsfrekvens for å matche videoinnhold i fullskjerm", + "matchDynamicRange": "Tilpass dynamisk område", + "matchDynamicRangeDescription": "Aktiver HDR automatisk for HDR-innhold og gå tilbake til SDR når spilleren lukkes", + "displaySwitchDelay": "Forsinkelse ved skjermbytte", + "displaySwitchDelayDescription": "Sekunder å vente etter en skjermendring før avspilling starter", "tunneledPlayback": "Tunnelert avspilling", "tunneledPlaybackDescription": "Bruk maskinvareakselerert videotunnelering. Deaktiver hvis du ser svart skjerm med lyd på HDR-innhold", "requireProfileSelectionOnOpen": "Spør om profil ved appåpning", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index ae73c5a1..665837cf 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -211,6 +211,12 @@ "autoPipDescription": "Automatisch beeld-in-beeld activeren bij het verlaten van de app tijdens afspelen", "matchContentFrameRate": "Inhoudsframesnelheid afstemmen", "matchContentFrameRateDescription": "Pas de schermverversingssnelheid aan op de video-inhoud, vermindert haperingen en bespaart batterij", + "matchRefreshRate": "Verversingssnelheid afstemmen", + "matchRefreshRateDescription": "Schermverversingssnelheid aanpassen aan de video-inhoud op volledig scherm", + "matchDynamicRange": "Dynamisch bereik afstemmen", + "matchDynamicRangeDescription": "HDR automatisch inschakelen voor HDR-inhoud en terugkeren naar SDR bij het verlaten van de speler", + "displaySwitchDelay": "Vertraging bij schermwisseling", + "displaySwitchDelayDescription": "Seconden wachten na een schermwisseling voordat het afspelen begint", "tunneledPlayback": "Getunnelde weergave", "tunneledPlaybackDescription": "Gebruik hardwareversnelde videotunneling. Schakel uit als je een zwart scherm met geluid ziet bij HDR-content", "requireProfileSelectionOnOpen": "Vraag om profiel bij openen", diff --git a/lib/i18n/pl.i18n.json b/lib/i18n/pl.i18n.json index 648e151d..dfda01d7 100644 --- a/lib/i18n/pl.i18n.json +++ b/lib/i18n/pl.i18n.json @@ -211,6 +211,12 @@ "autoPipDescription": "Automatycznie przejdź do trybu obraz w obrazie przy wyjściu z aplikacji podczas odtwarzania", "matchContentFrameRate": "Dopasuj częstotliwość klatek do treści", "matchContentFrameRateDescription": "Dostosuj częstotliwość odświeżania ekranu do treści wideo, zmniejszając drgania i oszczędzając baterię", + "matchRefreshRate": "Dopasuj częstotliwość odświeżania", + "matchRefreshRateDescription": "Przełącz częstotliwość odświeżania ekranu, aby dopasować ją do treści wideo na pełnym ekranie", + "matchDynamicRange": "Dopasuj zakres dynamiki", + "matchDynamicRangeDescription": "Automatycznie włącz HDR dla treści HDR i przywróć SDR po wyjściu z odtwarzacza", + "displaySwitchDelay": "Opóźnienie przełączania ekranu", + "displaySwitchDelayDescription": "Sekundy oczekiwania po zmianie trybu ekranu przed rozpoczęciem odtwarzania", "tunneledPlayback": "Tunelowane odtwarzanie", "tunneledPlaybackDescription": "Użyj sprzętowo przyspieszonego tunelowania wideo. Wyłącz, jeśli widzisz czarny ekran z dźwiękiem przy treściach HDR", "requireProfileSelectionOnOpen": "Pytaj o profil przy otwarciu aplikacji", diff --git a/lib/i18n/pt.i18n.json b/lib/i18n/pt.i18n.json index 258d37e4..ec2b95a0 100644 --- a/lib/i18n/pt.i18n.json +++ b/lib/i18n/pt.i18n.json @@ -211,6 +211,12 @@ "autoPipDescription": "Entrar automaticamente em picture-in-picture ao sair do app durante a reprodução", "matchContentFrameRate": "Corresponder Taxa de Quadros do Conteúdo", "matchContentFrameRateDescription": "Ajustar a taxa de atualização da tela para corresponder ao conteúdo de vídeo, reduzindo tremulação e economizando bateria", + "matchRefreshRate": "Corresponder Taxa de Atualização", + "matchRefreshRateDescription": "Alterar a taxa de atualização da tela para corresponder ao conteúdo de vídeo em tela cheia", + "matchDynamicRange": "Corresponder Faixa Dinâmica", + "matchDynamicRangeDescription": "Ativar automaticamente o HDR para conteúdo HDR e voltar ao SDR ao sair do reprodutor", + "displaySwitchDelay": "Atraso na Troca de Tela", + "displaySwitchDelayDescription": "Segundos de espera após uma troca de tela antes de iniciar a reprodução", "tunneledPlayback": "Reprodução Tunelizada", "tunneledPlaybackDescription": "Usar tunelamento de vídeo acelerado por hardware. Desative se você vir uma tela preta com áudio em conteúdo HDR", "requireProfileSelectionOnOpen": "Pedir perfil ao abrir o app", diff --git a/lib/i18n/ru.i18n.json b/lib/i18n/ru.i18n.json index 994c61be..1d94e6d4 100644 --- a/lib/i18n/ru.i18n.json +++ b/lib/i18n/ru.i18n.json @@ -211,6 +211,12 @@ "autoPipDescription": "Автоматически переходить в режим «картинка в картинке» при выходе из приложения во время воспроизведения", "matchContentFrameRate": "Соответствие частоты кадров контента", "matchContentFrameRateDescription": "Настроить частоту обновления дисплея под видеоконтент, уменьшая дрожание и экономя батарею", + "matchRefreshRate": "Соответствие частоты обновления", + "matchRefreshRateDescription": "Переключать частоту обновления дисплея под видеоконтент в полноэкранном режиме", + "matchDynamicRange": "Соответствие динамического диапазона", + "matchDynamicRangeDescription": "Автоматически включать HDR для HDR-контента и возвращать SDR при выходе из плеера", + "displaySwitchDelay": "Задержка переключения дисплея", + "displaySwitchDelayDescription": "Секунды ожидания после смены режима дисплея перед началом воспроизведения", "tunneledPlayback": "Туннельное воспроизведение", "tunneledPlaybackDescription": "Использовать аппаратный видеотуннелинг. Отключите, если видите чёрный экран со звуком при HDR-контенте", "requireProfileSelectionOnOpen": "Запрашивать профиль при запуске", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index d0d00af9..3ddf781f 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 15 -/// Strings: 12585 (839 per locale) +/// Strings: 12675 (845 per locale) /// -/// Built on 2026-04-01 at 22:20 UTC +/// Built on 2026-04-02 at 10:38 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_da.g.dart b/lib/i18n/strings_da.g.dart index bc2afebc..1b437151 100644 --- a/lib/i18n/strings_da.g.dart +++ b/lib/i18n/strings_da.g.dart @@ -330,6 +330,12 @@ class _TranslationsSettingsDa implements TranslationsSettingsEn { @override String get autoPipDescription => 'Gå automatisk til billede-i-billede når du forlader appen under afspilning'; @override String get matchContentFrameRate => 'Match indholdets billedhastighed'; @override String get matchContentFrameRateDescription => 'Juster skærmens opdateringshastighed til videoindhold for at reducere hakken og spare batteri'; + @override String get matchRefreshRate => 'Match opdateringshastighed'; + @override String get matchRefreshRateDescription => 'Skift skærmens opdateringshastighed til at matche videoindhold i fuldskærm'; + @override String get matchDynamicRange => 'Match dynamisk område'; + @override String get matchDynamicRangeDescription => 'Aktiver automatisk HDR for HDR-indhold og gendan SDR når afspilleren lukkes'; + @override String get displaySwitchDelay => 'Forsinkelse ved skærmskift'; + @override String get displaySwitchDelayDescription => 'Sekunder der ventes efter et skærmskift før afspilning starter'; @override String get tunneledPlayback => 'Tunneleret afspilning'; @override String get tunneledPlaybackDescription => 'Brug hardwareaccelereret videotunneling. Deaktiver hvis du ser sort skærm med lyd på HDR-indhold'; @override String get requireProfileSelectionOnOpen => 'Spørg om profil ved åbning'; @@ -1537,6 +1543,12 @@ extension on TranslationsDa { 'settings.autoPipDescription' => 'Gå automatisk til billede-i-billede når du forlader appen under afspilning', 'settings.matchContentFrameRate' => 'Match indholdets billedhastighed', 'settings.matchContentFrameRateDescription' => 'Juster skærmens opdateringshastighed til videoindhold for at reducere hakken og spare batteri', + 'settings.matchRefreshRate' => 'Match opdateringshastighed', + 'settings.matchRefreshRateDescription' => 'Skift skærmens opdateringshastighed til at matche videoindhold i fuldskærm', + 'settings.matchDynamicRange' => 'Match dynamisk område', + 'settings.matchDynamicRangeDescription' => 'Aktiver automatisk HDR for HDR-indhold og gendan SDR når afspilleren lukkes', + 'settings.displaySwitchDelay' => 'Forsinkelse ved skærmskift', + 'settings.displaySwitchDelayDescription' => 'Sekunder der ventes efter et skærmskift før afspilning starter', 'settings.tunneledPlayback' => 'Tunneleret afspilning', 'settings.tunneledPlaybackDescription' => 'Brug hardwareaccelereret videotunneling. Deaktiver hvis du ser sort skærm med lyd på HDR-indhold', 'settings.requireProfileSelectionOnOpen' => 'Spørg om profil ved åbning', @@ -1842,14 +1854,14 @@ extension on TranslationsDa { 'hubDetail.dateAdded' => 'Tilføjelsesdato', 'hubDetail.rating' => 'Bedømmelse', 'hubDetail.noItemsFound' => 'Ingen elementer fundet', + _ => null, + } ?? switch (path) { 'logs.clearLogs' => 'Ryd logs', 'logs.copyLogs' => 'Kopiér logs', 'logs.uploadLogs' => 'Upload logs', 'licenses.relatedPackages' => 'Relaterede pakker', 'licenses.license' => 'Licens', 'licenses.licenseNumber' => ({required Object number}) => 'Licens ${number}', - _ => null, - } ?? switch (path) { 'licenses.licensesCount' => ({required Object count}) => '${count} licenser', 'navigation.libraries' => 'Biblioteker', 'navigation.downloads' => 'Downloads', diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index c1e4bffb..ac4f6d0c 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -330,6 +330,12 @@ class _TranslationsSettingsDe implements TranslationsSettingsEn { @override String get autoPipDescription => 'Automatisch Bild-in-Bild aktivieren, wenn die App während der Wiedergabe verlassen wird'; @override String get matchContentFrameRate => 'Inhalts-Bildrate anpassen'; @override String get matchContentFrameRateDescription => 'Bildwiederholfrequenz des Displays an den Videoinhalt anpassen, reduziert Ruckeln und spart Akku'; + @override String get matchRefreshRate => 'Bildwiederholrate anpassen'; + @override String get matchRefreshRateDescription => 'Bildwiederholrate des Displays an den Videoinhalt im Vollbildmodus anpassen'; + @override String get matchDynamicRange => 'Dynamikumfang anpassen'; + @override String get matchDynamicRangeDescription => 'HDR für HDR-Inhalte automatisch aktivieren und beim Verlassen des Players auf SDR zurücksetzen'; + @override String get displaySwitchDelay => 'Verzögerung beim Displaywechsel'; + @override String get displaySwitchDelayDescription => 'Sekunden Wartezeit nach einem Displaywechsel vor Wiedergabebeginn'; @override String get tunneledPlayback => 'Tunnelwiedergabe'; @override String get tunneledPlaybackDescription => 'Hardware-beschleunigte Video-Tunnelierung verwenden. Deaktivieren, wenn bei HDR-Inhalten ein schwarzer Bildschirm mit Ton erscheint'; @override String get requireProfileSelectionOnOpen => 'Profil beim Öffnen abfragen'; @@ -1537,6 +1543,12 @@ extension on TranslationsDe { 'settings.autoPipDescription' => 'Automatisch Bild-in-Bild aktivieren, wenn die App während der Wiedergabe verlassen wird', 'settings.matchContentFrameRate' => 'Inhalts-Bildrate anpassen', 'settings.matchContentFrameRateDescription' => 'Bildwiederholfrequenz des Displays an den Videoinhalt anpassen, reduziert Ruckeln und spart Akku', + 'settings.matchRefreshRate' => 'Bildwiederholrate anpassen', + 'settings.matchRefreshRateDescription' => 'Bildwiederholrate des Displays an den Videoinhalt im Vollbildmodus anpassen', + 'settings.matchDynamicRange' => 'Dynamikumfang anpassen', + 'settings.matchDynamicRangeDescription' => 'HDR für HDR-Inhalte automatisch aktivieren und beim Verlassen des Players auf SDR zurücksetzen', + 'settings.displaySwitchDelay' => 'Verzögerung beim Displaywechsel', + 'settings.displaySwitchDelayDescription' => 'Sekunden Wartezeit nach einem Displaywechsel vor Wiedergabebeginn', 'settings.tunneledPlayback' => 'Tunnelwiedergabe', 'settings.tunneledPlaybackDescription' => 'Hardware-beschleunigte Video-Tunnelierung verwenden. Deaktivieren, wenn bei HDR-Inhalten ein schwarzer Bildschirm mit Ton erscheint', 'settings.requireProfileSelectionOnOpen' => 'Profil beim Öffnen abfragen', @@ -1842,14 +1854,14 @@ extension on TranslationsDe { 'hubDetail.dateAdded' => 'Hinzugefügt am', 'hubDetail.rating' => 'Bewertung', 'hubDetail.noItemsFound' => 'Keine Elemente gefunden', + _ => null, + } ?? switch (path) { 'logs.clearLogs' => 'Protokolle löschen', 'logs.copyLogs' => 'Protokolle kopieren', 'logs.uploadLogs' => 'Protokolle hochladen', 'licenses.relatedPackages' => 'Verwandte Pakete', 'licenses.license' => 'Lizenz', 'licenses.licenseNumber' => ({required Object number}) => 'Lizenz ${number}', - _ => null, - } ?? switch (path) { 'licenses.licensesCount' => ({required Object count}) => '${count} Lizenzen', 'navigation.libraries' => 'Medien', 'navigation.downloads' => 'Downloads', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index d7b764d3..b0325e34 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -736,6 +736,24 @@ class TranslationsSettingsEn { /// en: 'Adjust display refresh rate to match video content, reducing judder and saving battery' String get matchContentFrameRateDescription => 'Adjust display refresh rate to match video content, reducing judder and saving battery'; + /// en: 'Match Refresh Rate' + String get matchRefreshRate => 'Match Refresh Rate'; + + /// en: 'Switch display refresh rate to match video content when in fullscreen' + String get matchRefreshRateDescription => 'Switch display refresh rate to match video content when in fullscreen'; + + /// en: 'Match Dynamic Range' + String get matchDynamicRange => 'Match Dynamic Range'; + + /// en: 'Auto-enable HDR for HDR content and revert to SDR when leaving the player' + String get matchDynamicRangeDescription => 'Auto-enable HDR for HDR content and revert to SDR when leaving the player'; + + /// en: 'Display Switch Delay' + String get displaySwitchDelay => 'Display Switch Delay'; + + /// en: 'Seconds to wait after a display mode change before starting playback' + String get displaySwitchDelayDescription => 'Seconds to wait after a display mode change before starting playback'; + /// en: 'Tunneled Playback' String get tunneledPlayback => 'Tunneled Playback'; @@ -3222,6 +3240,12 @@ extension on Translations { 'settings.autoPipDescription' => 'Automatically enter picture-in-picture when leaving the app during playback', 'settings.matchContentFrameRate' => 'Match Content Frame Rate', 'settings.matchContentFrameRateDescription' => 'Adjust display refresh rate to match video content, reducing judder and saving battery', + 'settings.matchRefreshRate' => 'Match Refresh Rate', + 'settings.matchRefreshRateDescription' => 'Switch display refresh rate to match video content when in fullscreen', + 'settings.matchDynamicRange' => 'Match Dynamic Range', + 'settings.matchDynamicRangeDescription' => 'Auto-enable HDR for HDR content and revert to SDR when leaving the player', + 'settings.displaySwitchDelay' => 'Display Switch Delay', + 'settings.displaySwitchDelayDescription' => 'Seconds to wait after a display mode change before starting playback', 'settings.tunneledPlayback' => 'Tunneled Playback', 'settings.tunneledPlaybackDescription' => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content', 'settings.requireProfileSelectionOnOpen' => 'Ask for profile on app open', @@ -3527,14 +3551,14 @@ extension on Translations { 'hubDetail.dateAdded' => 'Date Added', 'hubDetail.rating' => 'Rating', 'hubDetail.noItemsFound' => 'No items found', + _ => null, + } ?? switch (path) { 'logs.clearLogs' => 'Clear Logs', 'logs.copyLogs' => 'Copy Logs', 'logs.uploadLogs' => 'Upload Logs', 'licenses.relatedPackages' => 'Related Packages', 'licenses.license' => 'License', 'licenses.licenseNumber' => ({required Object number}) => 'License ${number}', - _ => null, - } ?? switch (path) { 'licenses.licensesCount' => ({required Object count}) => '${count} licenses', 'navigation.libraries' => 'Libraries', 'navigation.downloads' => 'Downloads', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index d138e676..64151699 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -330,6 +330,12 @@ class _TranslationsSettingsEs implements TranslationsSettingsEn { @override String get autoPipDescription => 'Activar automáticamente imagen en imagen al salir de la app durante la reproducción'; @override String get matchContentFrameRate => 'Ajustar frecuencia de actualización'; @override String get matchContentFrameRateDescription => 'Ajustar la frecuencia de actualización de la pantalla para que coincida con el video, reduciendo tirones y ahorrando batería'; + @override String get matchRefreshRate => 'Ajustar frecuencia de refresco'; + @override String get matchRefreshRateDescription => 'Cambiar la frecuencia de refresco de la pantalla para coincidir con el contenido de video en pantalla completa'; + @override String get matchDynamicRange => 'Ajustar rango dinámico'; + @override String get matchDynamicRangeDescription => 'Activar HDR automáticamente para contenido HDR y volver a SDR al salir del reproductor'; + @override String get displaySwitchDelay => 'Retraso de cambio de pantalla'; + @override String get displaySwitchDelayDescription => 'Segundos de espera después de un cambio de pantalla antes de iniciar la reproducción'; @override String get tunneledPlayback => 'Reproducción tunelizada'; @override String get tunneledPlaybackDescription => 'Usar tunelización de video acelerada por hardware. Desactivar si ves una pantalla negra con audio en contenido HDR'; @override String get requireProfileSelectionOnOpen => 'Pedir perfil al abrir la app'; @@ -1537,6 +1543,12 @@ extension on TranslationsEs { 'settings.autoPipDescription' => 'Activar automáticamente imagen en imagen al salir de la app durante la reproducción', 'settings.matchContentFrameRate' => 'Ajustar frecuencia de actualización', 'settings.matchContentFrameRateDescription' => 'Ajustar la frecuencia de actualización de la pantalla para que coincida con el video, reduciendo tirones y ahorrando batería', + 'settings.matchRefreshRate' => 'Ajustar frecuencia de refresco', + 'settings.matchRefreshRateDescription' => 'Cambiar la frecuencia de refresco de la pantalla para coincidir con el contenido de video en pantalla completa', + 'settings.matchDynamicRange' => 'Ajustar rango dinámico', + 'settings.matchDynamicRangeDescription' => 'Activar HDR automáticamente para contenido HDR y volver a SDR al salir del reproductor', + 'settings.displaySwitchDelay' => 'Retraso de cambio de pantalla', + 'settings.displaySwitchDelayDescription' => 'Segundos de espera después de un cambio de pantalla antes de iniciar la reproducción', 'settings.tunneledPlayback' => 'Reproducción tunelizada', 'settings.tunneledPlaybackDescription' => 'Usar tunelización de video acelerada por hardware. Desactivar si ves una pantalla negra con audio en contenido HDR', 'settings.requireProfileSelectionOnOpen' => 'Pedir perfil al abrir la app', @@ -1842,14 +1854,14 @@ extension on TranslationsEs { 'hubDetail.dateAdded' => 'Añadido el', 'hubDetail.rating' => 'Calificación', 'hubDetail.noItemsFound' => 'No se encontraron elementos', + _ => null, + } ?? switch (path) { 'logs.clearLogs' => 'Borrar Logs', 'logs.copyLogs' => 'Copiar Logs', 'logs.uploadLogs' => 'Subir registros', 'licenses.relatedPackages' => 'Paquetes relacionados', 'licenses.license' => 'Licencia', 'licenses.licenseNumber' => ({required Object number}) => 'Licencia ${number}', - _ => null, - } ?? switch (path) { 'licenses.licensesCount' => ({required Object count}) => '${count} licencias', 'navigation.libraries' => 'Medios', 'navigation.downloads' => 'Descargas', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index 9b6b6f8d..0fcbffad 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -330,6 +330,12 @@ class _TranslationsSettingsFr implements TranslationsSettingsEn { @override String get autoPipDescription => 'Activer automatiquement l\'image dans l\'image en quittant l\'application pendant la lecture'; @override String get matchContentFrameRate => 'Fréquence d\'images du contenu correspondant'; @override String get matchContentFrameRateDescription => 'Ajustez la fréquence de rafraîchissement de l\'écran en fonction du contenu vidéo, ce qui réduit les saccades et économise la batterie'; + @override String get matchRefreshRate => 'Adapter la fréquence de rafraîchissement'; + @override String get matchRefreshRateDescription => 'Changer la fréquence de rafraîchissement de l\'écran pour correspondre au contenu vidéo en plein écran'; + @override String get matchDynamicRange => 'Adapter la plage dynamique'; + @override String get matchDynamicRangeDescription => 'Activer automatiquement le HDR pour le contenu HDR et revenir en SDR en quittant le lecteur'; + @override String get displaySwitchDelay => 'Délai de changement d\'affichage'; + @override String get displaySwitchDelayDescription => 'Secondes d\'attente après un changement d\'affichage avant de démarrer la lecture'; @override String get tunneledPlayback => 'Lecture tunnelée'; @override String get tunneledPlaybackDescription => 'Utiliser le tunnelage vidéo accéléré par matériel. Désactiver si vous voyez un écran noir avec du son sur du contenu HDR'; @override String get requireProfileSelectionOnOpen => 'Demander le profil à l\'ouverture'; @@ -1537,6 +1543,12 @@ extension on TranslationsFr { 'settings.autoPipDescription' => 'Activer automatiquement l\'image dans l\'image en quittant l\'application pendant la lecture', 'settings.matchContentFrameRate' => 'Fréquence d\'images du contenu correspondant', 'settings.matchContentFrameRateDescription' => 'Ajustez la fréquence de rafraîchissement de l\'écran en fonction du contenu vidéo, ce qui réduit les saccades et économise la batterie', + 'settings.matchRefreshRate' => 'Adapter la fréquence de rafraîchissement', + 'settings.matchRefreshRateDescription' => 'Changer la fréquence de rafraîchissement de l\'écran pour correspondre au contenu vidéo en plein écran', + 'settings.matchDynamicRange' => 'Adapter la plage dynamique', + 'settings.matchDynamicRangeDescription' => 'Activer automatiquement le HDR pour le contenu HDR et revenir en SDR en quittant le lecteur', + 'settings.displaySwitchDelay' => 'Délai de changement d\'affichage', + 'settings.displaySwitchDelayDescription' => 'Secondes d\'attente après un changement d\'affichage avant de démarrer la lecture', 'settings.tunneledPlayback' => 'Lecture tunnelée', 'settings.tunneledPlaybackDescription' => 'Utiliser le tunnelage vidéo accéléré par matériel. Désactiver si vous voyez un écran noir avec du son sur du contenu HDR', 'settings.requireProfileSelectionOnOpen' => 'Demander le profil à l\'ouverture', @@ -1842,14 +1854,14 @@ extension on TranslationsFr { 'hubDetail.dateAdded' => 'Date d\'ajout', 'hubDetail.rating' => 'Évaluation', 'hubDetail.noItemsFound' => 'Aucun élément trouvé', + _ => null, + } ?? switch (path) { 'logs.clearLogs' => 'Effacer les logs', 'logs.copyLogs' => 'Copier les logs', 'logs.uploadLogs' => 'Envoyer les logs', 'licenses.relatedPackages' => 'Package associés', 'licenses.license' => 'Licence', 'licenses.licenseNumber' => ({required Object number}) => 'Licence ${number}', - _ => null, - } ?? switch (path) { 'licenses.licensesCount' => ({required Object count}) => '${count} licences', 'navigation.libraries' => 'Medias', 'navigation.downloads' => 'Téléch.', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index 65c8393c..6d8f1021 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -330,6 +330,12 @@ class _TranslationsSettingsIt implements TranslationsSettingsEn { @override String get autoPipDescription => 'Attiva automaticamente il picture-in-picture quando si esce dall\'app durante la riproduzione'; @override String get matchContentFrameRate => 'Adatta frequenza fotogrammi'; @override String get matchContentFrameRateDescription => 'Regola la frequenza di aggiornamento del display in base al contenuto video, riducendo i tremolii e risparmiando batteria'; + @override String get matchRefreshRate => 'Adatta frequenza di aggiornamento'; + @override String get matchRefreshRateDescription => 'Cambia la frequenza di aggiornamento del display per adattarla al contenuto video a schermo intero'; + @override String get matchDynamicRange => 'Adatta gamma dinamica'; + @override String get matchDynamicRangeDescription => 'Attiva automaticamente l\'HDR per i contenuti HDR e torna in SDR all\'uscita dal lettore'; + @override String get displaySwitchDelay => 'Ritardo cambio display'; + @override String get displaySwitchDelayDescription => 'Secondi di attesa dopo un cambio di display prima di avviare la riproduzione'; @override String get tunneledPlayback => 'Riproduzione tunnelizzata'; @override String get tunneledPlaybackDescription => 'Usa il tunneling video accelerato dall\'hardware. Disattiva se vedi uno schermo nero con audio su contenuti HDR'; @override String get requireProfileSelectionOnOpen => 'Chiedi profilo all\'apertura'; @@ -1537,6 +1543,12 @@ extension on TranslationsIt { 'settings.autoPipDescription' => 'Attiva automaticamente il picture-in-picture quando si esce dall\'app durante la riproduzione', 'settings.matchContentFrameRate' => 'Adatta frequenza fotogrammi', 'settings.matchContentFrameRateDescription' => 'Regola la frequenza di aggiornamento del display in base al contenuto video, riducendo i tremolii e risparmiando batteria', + 'settings.matchRefreshRate' => 'Adatta frequenza di aggiornamento', + 'settings.matchRefreshRateDescription' => 'Cambia la frequenza di aggiornamento del display per adattarla al contenuto video a schermo intero', + 'settings.matchDynamicRange' => 'Adatta gamma dinamica', + 'settings.matchDynamicRangeDescription' => 'Attiva automaticamente l\'HDR per i contenuti HDR e torna in SDR all\'uscita dal lettore', + 'settings.displaySwitchDelay' => 'Ritardo cambio display', + 'settings.displaySwitchDelayDescription' => 'Secondi di attesa dopo un cambio di display prima di avviare la riproduzione', 'settings.tunneledPlayback' => 'Riproduzione tunnelizzata', 'settings.tunneledPlaybackDescription' => 'Usa il tunneling video accelerato dall\'hardware. Disattiva se vedi uno schermo nero con audio su contenuti HDR', 'settings.requireProfileSelectionOnOpen' => 'Chiedi profilo all\'apertura', @@ -1842,14 +1854,14 @@ extension on TranslationsIt { 'hubDetail.dateAdded' => 'Data aggiunta', 'hubDetail.rating' => 'Valutazione', 'hubDetail.noItemsFound' => 'Nessun elemento trovato', + _ => null, + } ?? switch (path) { 'logs.clearLogs' => 'Cancella log', 'logs.copyLogs' => 'Copia log', 'logs.uploadLogs' => 'Carica log', 'licenses.relatedPackages' => 'Pacchetti correlati', 'licenses.license' => 'Licenza', 'licenses.licenseNumber' => ({required Object number}) => 'Licenza ${number}', - _ => null, - } ?? switch (path) { 'licenses.licensesCount' => ({required Object count}) => '${count} licenze', 'navigation.libraries' => 'Librerie', 'navigation.downloads' => 'Download', diff --git a/lib/i18n/strings_ja.g.dart b/lib/i18n/strings_ja.g.dart index 5cab00ba..b574811d 100644 --- a/lib/i18n/strings_ja.g.dart +++ b/lib/i18n/strings_ja.g.dart @@ -330,6 +330,12 @@ class _TranslationsSettingsJa implements TranslationsSettingsEn { @override String get autoPipDescription => '再生中にアプリを離れると自動的にピクチャーインピクチャーに移行'; @override String get matchContentFrameRate => 'コンテンツのフレームレートに合わせる'; @override String get matchContentFrameRateDescription => '動画コンテンツに合わせてディスプレイのリフレッシュレートを調整し、ジャダーを低減しバッテリーを節約'; + @override String get matchRefreshRate => 'リフレッシュレートを合わせる'; + @override String get matchRefreshRateDescription => 'フルスクリーン時にディスプレイのリフレッシュレートを動画コンテンツに合わせて切り替える'; + @override String get matchDynamicRange => 'ダイナミックレンジを合わせる'; + @override String get matchDynamicRangeDescription => 'HDRコンテンツに対して自動的にHDRを有効にし、プレーヤーを閉じるとSDRに戻す'; + @override String get displaySwitchDelay => 'ディスプレイ切り替え遅延'; + @override String get displaySwitchDelayDescription => 'ディスプレイモード変更後、再生を開始するまでの待機秒数'; @override String get tunneledPlayback => 'トンネル再生'; @override String get tunneledPlaybackDescription => 'ハードウェアアクセラレーションされたビデオトンネリングを使用。HDRコンテンツで音声のみで画面が黒くなる場合は無効にしてください'; @override String get requireProfileSelectionOnOpen => 'アプリ起動時にプロフィールを確認'; @@ -1537,6 +1543,12 @@ extension on TranslationsJa { 'settings.autoPipDescription' => '再生中にアプリを離れると自動的にピクチャーインピクチャーに移行', 'settings.matchContentFrameRate' => 'コンテンツのフレームレートに合わせる', 'settings.matchContentFrameRateDescription' => '動画コンテンツに合わせてディスプレイのリフレッシュレートを調整し、ジャダーを低減しバッテリーを節約', + 'settings.matchRefreshRate' => 'リフレッシュレートを合わせる', + 'settings.matchRefreshRateDescription' => 'フルスクリーン時にディスプレイのリフレッシュレートを動画コンテンツに合わせて切り替える', + 'settings.matchDynamicRange' => 'ダイナミックレンジを合わせる', + 'settings.matchDynamicRangeDescription' => 'HDRコンテンツに対して自動的にHDRを有効にし、プレーヤーを閉じるとSDRに戻す', + 'settings.displaySwitchDelay' => 'ディスプレイ切り替え遅延', + 'settings.displaySwitchDelayDescription' => 'ディスプレイモード変更後、再生を開始するまでの待機秒数', 'settings.tunneledPlayback' => 'トンネル再生', 'settings.tunneledPlaybackDescription' => 'ハードウェアアクセラレーションされたビデオトンネリングを使用。HDRコンテンツで音声のみで画面が黒くなる場合は無効にしてください', 'settings.requireProfileSelectionOnOpen' => 'アプリ起動時にプロフィールを確認', @@ -1842,14 +1854,14 @@ extension on TranslationsJa { 'hubDetail.dateAdded' => '追加日', 'hubDetail.rating' => '評価', 'hubDetail.noItemsFound' => 'アイテムが見つかりません', + _ => null, + } ?? switch (path) { 'logs.clearLogs' => 'ログをクリア', 'logs.copyLogs' => 'ログをコピー', 'logs.uploadLogs' => 'ログをアップロード', 'licenses.relatedPackages' => '関連パッケージ', 'licenses.license' => 'ライセンス', 'licenses.licenseNumber' => ({required Object number}) => 'ライセンス ${number}', - _ => null, - } ?? switch (path) { 'licenses.licensesCount' => ({required Object count}) => '${count}件のライセンス', 'navigation.libraries' => 'ライブラリ', 'navigation.downloads' => 'ダウンロード', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index 03981190..09718809 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -330,6 +330,12 @@ class _TranslationsSettingsKo implements TranslationsSettingsEn { @override String get autoPipDescription => '재생 중 앱을 나갈 때 자동으로 PIP 모드로 전환'; @override String get matchContentFrameRate => '콘텐츠 프레임 레이트 맞춤'; @override String get matchContentFrameRateDescription => '비디오 콘텐츠에 맞게 디스플레이 주사율을 조정하여 떨림을 줄이고 배터리를 절약합니다'; + @override String get matchRefreshRate => '주사율 맞춤'; + @override String get matchRefreshRateDescription => '전체 화면에서 비디오 콘텐츠에 맞게 디스플레이 주사율을 전환합니다'; + @override String get matchDynamicRange => '다이나믹 레인지 맞춤'; + @override String get matchDynamicRangeDescription => 'HDR 콘텐츠에 대해 자동으로 HDR을 활성화하고 플레이어를 나갈 때 SDR로 복원합니다'; + @override String get displaySwitchDelay => '디스플레이 전환 지연'; + @override String get displaySwitchDelayDescription => '디스플레이 모드 변경 후 재생을 시작하기까지 대기하는 시간(초)'; @override String get tunneledPlayback => '터널 재생'; @override String get tunneledPlaybackDescription => '하드웨어 가속 비디오 터널링을 사용합니다. HDR 콘텐츠에서 소리만 나고 검은 화면이 보이면 비활성화하세요'; @override String get requireProfileSelectionOnOpen => '앱 실행 시 프로필 선택'; @@ -1537,6 +1543,12 @@ extension on TranslationsKo { 'settings.autoPipDescription' => '재생 중 앱을 나갈 때 자동으로 PIP 모드로 전환', 'settings.matchContentFrameRate' => '콘텐츠 프레임 레이트 맞춤', 'settings.matchContentFrameRateDescription' => '비디오 콘텐츠에 맞게 디스플레이 주사율을 조정하여 떨림을 줄이고 배터리를 절약합니다', + 'settings.matchRefreshRate' => '주사율 맞춤', + 'settings.matchRefreshRateDescription' => '전체 화면에서 비디오 콘텐츠에 맞게 디스플레이 주사율을 전환합니다', + 'settings.matchDynamicRange' => '다이나믹 레인지 맞춤', + 'settings.matchDynamicRangeDescription' => 'HDR 콘텐츠에 대해 자동으로 HDR을 활성화하고 플레이어를 나갈 때 SDR로 복원합니다', + 'settings.displaySwitchDelay' => '디스플레이 전환 지연', + 'settings.displaySwitchDelayDescription' => '디스플레이 모드 변경 후 재생을 시작하기까지 대기하는 시간(초)', 'settings.tunneledPlayback' => '터널 재생', 'settings.tunneledPlaybackDescription' => '하드웨어 가속 비디오 터널링을 사용합니다. HDR 콘텐츠에서 소리만 나고 검은 화면이 보이면 비활성화하세요', 'settings.requireProfileSelectionOnOpen' => '앱 실행 시 프로필 선택', @@ -1842,14 +1854,14 @@ extension on TranslationsKo { 'hubDetail.dateAdded' => '추가 날짜', 'hubDetail.rating' => '평점', 'hubDetail.noItemsFound' => '항목이 없습니다', + _ => null, + } ?? switch (path) { 'logs.clearLogs' => '로그 지우기', 'logs.copyLogs' => '로그 복사', 'logs.uploadLogs' => '로그 업로드', 'licenses.relatedPackages' => '관련 소프트웨어 패키지', 'licenses.license' => '라이선스', 'licenses.licenseNumber' => ({required Object number}) => '라이선스 ${number}', - _ => null, - } ?? switch (path) { 'licenses.licensesCount' => ({required Object count}) => '${count} 개의 라이선스', 'navigation.libraries' => '미디어 라이브러리', 'navigation.downloads' => '다운로드', diff --git a/lib/i18n/strings_nb.g.dart b/lib/i18n/strings_nb.g.dart index 1cde300d..b9fb5dbe 100644 --- a/lib/i18n/strings_nb.g.dart +++ b/lib/i18n/strings_nb.g.dart @@ -330,6 +330,12 @@ class _TranslationsSettingsNb implements TranslationsSettingsEn { @override String get autoPipDescription => 'Gå automatisk til bilde-i-bilde når du forlater appen under avspilling'; @override String get matchContentFrameRate => 'Tilpass innholdets bildefrekvens'; @override String get matchContentFrameRateDescription => 'Juster skjermens oppdateringsfrekvens for å matche videoinnhold, reduserer hakking og sparer batteri'; + @override String get matchRefreshRate => 'Tilpass oppdateringsfrekvens'; + @override String get matchRefreshRateDescription => 'Bytt skjermens oppdateringsfrekvens for å matche videoinnhold i fullskjerm'; + @override String get matchDynamicRange => 'Tilpass dynamisk område'; + @override String get matchDynamicRangeDescription => 'Aktiver HDR automatisk for HDR-innhold og gå tilbake til SDR når spilleren lukkes'; + @override String get displaySwitchDelay => 'Forsinkelse ved skjermbytte'; + @override String get displaySwitchDelayDescription => 'Sekunder å vente etter en skjermendring før avspilling starter'; @override String get tunneledPlayback => 'Tunnelert avspilling'; @override String get tunneledPlaybackDescription => 'Bruk maskinvareakselerert videotunnelering. Deaktiver hvis du ser svart skjerm med lyd på HDR-innhold'; @override String get requireProfileSelectionOnOpen => 'Spør om profil ved appåpning'; @@ -1537,6 +1543,12 @@ extension on TranslationsNb { 'settings.autoPipDescription' => 'Gå automatisk til bilde-i-bilde når du forlater appen under avspilling', 'settings.matchContentFrameRate' => 'Tilpass innholdets bildefrekvens', 'settings.matchContentFrameRateDescription' => 'Juster skjermens oppdateringsfrekvens for å matche videoinnhold, reduserer hakking og sparer batteri', + 'settings.matchRefreshRate' => 'Tilpass oppdateringsfrekvens', + 'settings.matchRefreshRateDescription' => 'Bytt skjermens oppdateringsfrekvens for å matche videoinnhold i fullskjerm', + 'settings.matchDynamicRange' => 'Tilpass dynamisk område', + 'settings.matchDynamicRangeDescription' => 'Aktiver HDR automatisk for HDR-innhold og gå tilbake til SDR når spilleren lukkes', + 'settings.displaySwitchDelay' => 'Forsinkelse ved skjermbytte', + 'settings.displaySwitchDelayDescription' => 'Sekunder å vente etter en skjermendring før avspilling starter', 'settings.tunneledPlayback' => 'Tunnelert avspilling', 'settings.tunneledPlaybackDescription' => 'Bruk maskinvareakselerert videotunnelering. Deaktiver hvis du ser svart skjerm med lyd på HDR-innhold', 'settings.requireProfileSelectionOnOpen' => 'Spør om profil ved appåpning', @@ -1842,14 +1854,14 @@ extension on TranslationsNb { 'hubDetail.dateAdded' => 'Dato lagt til', 'hubDetail.rating' => 'Vurdering', 'hubDetail.noItemsFound' => 'Ingen elementer funnet', + _ => null, + } ?? switch (path) { 'logs.clearLogs' => 'Tøm logger', 'logs.copyLogs' => 'Kopier logger', 'logs.uploadLogs' => 'Last opp logger', 'licenses.relatedPackages' => 'Relaterte pakker', 'licenses.license' => 'Lisens', 'licenses.licenseNumber' => ({required Object number}) => 'Lisens ${number}', - _ => null, - } ?? switch (path) { 'licenses.licensesCount' => ({required Object count}) => '${count} lisenser', 'navigation.libraries' => 'Biblioteker', 'navigation.downloads' => 'Nedlastinger', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index aec0ee70..0cdf58ab 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -330,6 +330,12 @@ class _TranslationsSettingsNl implements TranslationsSettingsEn { @override String get autoPipDescription => 'Automatisch beeld-in-beeld activeren bij het verlaten van de app tijdens afspelen'; @override String get matchContentFrameRate => 'Inhoudsframesnelheid afstemmen'; @override String get matchContentFrameRateDescription => 'Pas de schermverversingssnelheid aan op de video-inhoud, vermindert haperingen en bespaart batterij'; + @override String get matchRefreshRate => 'Verversingssnelheid afstemmen'; + @override String get matchRefreshRateDescription => 'Schermverversingssnelheid aanpassen aan de video-inhoud op volledig scherm'; + @override String get matchDynamicRange => 'Dynamisch bereik afstemmen'; + @override String get matchDynamicRangeDescription => 'HDR automatisch inschakelen voor HDR-inhoud en terugkeren naar SDR bij het verlaten van de speler'; + @override String get displaySwitchDelay => 'Vertraging bij schermwisseling'; + @override String get displaySwitchDelayDescription => 'Seconden wachten na een schermwisseling voordat het afspelen begint'; @override String get tunneledPlayback => 'Getunnelde weergave'; @override String get tunneledPlaybackDescription => 'Gebruik hardwareversnelde videotunneling. Schakel uit als je een zwart scherm met geluid ziet bij HDR-content'; @override String get requireProfileSelectionOnOpen => 'Vraag om profiel bij openen'; @@ -1537,6 +1543,12 @@ extension on TranslationsNl { 'settings.autoPipDescription' => 'Automatisch beeld-in-beeld activeren bij het verlaten van de app tijdens afspelen', 'settings.matchContentFrameRate' => 'Inhoudsframesnelheid afstemmen', 'settings.matchContentFrameRateDescription' => 'Pas de schermverversingssnelheid aan op de video-inhoud, vermindert haperingen en bespaart batterij', + 'settings.matchRefreshRate' => 'Verversingssnelheid afstemmen', + 'settings.matchRefreshRateDescription' => 'Schermverversingssnelheid aanpassen aan de video-inhoud op volledig scherm', + 'settings.matchDynamicRange' => 'Dynamisch bereik afstemmen', + 'settings.matchDynamicRangeDescription' => 'HDR automatisch inschakelen voor HDR-inhoud en terugkeren naar SDR bij het verlaten van de speler', + 'settings.displaySwitchDelay' => 'Vertraging bij schermwisseling', + 'settings.displaySwitchDelayDescription' => 'Seconden wachten na een schermwisseling voordat het afspelen begint', 'settings.tunneledPlayback' => 'Getunnelde weergave', 'settings.tunneledPlaybackDescription' => 'Gebruik hardwareversnelde videotunneling. Schakel uit als je een zwart scherm met geluid ziet bij HDR-content', 'settings.requireProfileSelectionOnOpen' => 'Vraag om profiel bij openen', @@ -1842,14 +1854,14 @@ extension on TranslationsNl { 'hubDetail.dateAdded' => 'Datum toegevoegd', 'hubDetail.rating' => 'Beoordeling', 'hubDetail.noItemsFound' => 'Geen items gevonden', + _ => null, + } ?? switch (path) { 'logs.clearLogs' => 'Wis logs', 'logs.copyLogs' => 'Kopieer logs', 'logs.uploadLogs' => 'Logs uploaden', 'licenses.relatedPackages' => 'Gerelateerde pakketten', 'licenses.license' => 'Licentie', 'licenses.licenseNumber' => ({required Object number}) => 'Licentie ${number}', - _ => null, - } ?? switch (path) { 'licenses.licensesCount' => ({required Object count}) => '${count} licenties', 'navigation.libraries' => 'Media', 'navigation.downloads' => 'Downloads', diff --git a/lib/i18n/strings_pl.g.dart b/lib/i18n/strings_pl.g.dart index 04a3e4e7..16a12c32 100644 --- a/lib/i18n/strings_pl.g.dart +++ b/lib/i18n/strings_pl.g.dart @@ -330,6 +330,12 @@ class _TranslationsSettingsPl implements TranslationsSettingsEn { @override String get autoPipDescription => 'Automatycznie przejdź do trybu obraz w obrazie przy wyjściu z aplikacji podczas odtwarzania'; @override String get matchContentFrameRate => 'Dopasuj częstotliwość klatek do treści'; @override String get matchContentFrameRateDescription => 'Dostosuj częstotliwość odświeżania ekranu do treści wideo, zmniejszając drgania i oszczędzając baterię'; + @override String get matchRefreshRate => 'Dopasuj częstotliwość odświeżania'; + @override String get matchRefreshRateDescription => 'Przełącz częstotliwość odświeżania ekranu, aby dopasować ją do treści wideo na pełnym ekranie'; + @override String get matchDynamicRange => 'Dopasuj zakres dynamiki'; + @override String get matchDynamicRangeDescription => 'Automatycznie włącz HDR dla treści HDR i przywróć SDR po wyjściu z odtwarzacza'; + @override String get displaySwitchDelay => 'Opóźnienie przełączania ekranu'; + @override String get displaySwitchDelayDescription => 'Sekundy oczekiwania po zmianie trybu ekranu przed rozpoczęciem odtwarzania'; @override String get tunneledPlayback => 'Tunelowane odtwarzanie'; @override String get tunneledPlaybackDescription => 'Użyj sprzętowo przyspieszonego tunelowania wideo. Wyłącz, jeśli widzisz czarny ekran z dźwiękiem przy treściach HDR'; @override String get requireProfileSelectionOnOpen => 'Pytaj o profil przy otwarciu aplikacji'; @@ -1537,6 +1543,12 @@ extension on TranslationsPl { 'settings.autoPipDescription' => 'Automatycznie przejdź do trybu obraz w obrazie przy wyjściu z aplikacji podczas odtwarzania', 'settings.matchContentFrameRate' => 'Dopasuj częstotliwość klatek do treści', 'settings.matchContentFrameRateDescription' => 'Dostosuj częstotliwość odświeżania ekranu do treści wideo, zmniejszając drgania i oszczędzając baterię', + 'settings.matchRefreshRate' => 'Dopasuj częstotliwość odświeżania', + 'settings.matchRefreshRateDescription' => 'Przełącz częstotliwość odświeżania ekranu, aby dopasować ją do treści wideo na pełnym ekranie', + 'settings.matchDynamicRange' => 'Dopasuj zakres dynamiki', + 'settings.matchDynamicRangeDescription' => 'Automatycznie włącz HDR dla treści HDR i przywróć SDR po wyjściu z odtwarzacza', + 'settings.displaySwitchDelay' => 'Opóźnienie przełączania ekranu', + 'settings.displaySwitchDelayDescription' => 'Sekundy oczekiwania po zmianie trybu ekranu przed rozpoczęciem odtwarzania', 'settings.tunneledPlayback' => 'Tunelowane odtwarzanie', 'settings.tunneledPlaybackDescription' => 'Użyj sprzętowo przyspieszonego tunelowania wideo. Wyłącz, jeśli widzisz czarny ekran z dźwiękiem przy treściach HDR', 'settings.requireProfileSelectionOnOpen' => 'Pytaj o profil przy otwarciu aplikacji', @@ -1842,14 +1854,14 @@ extension on TranslationsPl { 'hubDetail.dateAdded' => 'Data dodania', 'hubDetail.rating' => 'Ocena', 'hubDetail.noItemsFound' => 'Nie znaleziono elementów', + _ => null, + } ?? switch (path) { 'logs.clearLogs' => 'Wyczyść logi', 'logs.copyLogs' => 'Kopiuj logi', 'logs.uploadLogs' => 'Prześlij logi', 'licenses.relatedPackages' => 'Powiązane pakiety', 'licenses.license' => 'Licencja', 'licenses.licenseNumber' => ({required Object number}) => 'Licencja ${number}', - _ => null, - } ?? switch (path) { 'licenses.licensesCount' => ({required Object count}) => '${count} licencji', 'navigation.libraries' => 'Biblioteki', 'navigation.downloads' => 'Pobrania', diff --git a/lib/i18n/strings_pt.g.dart b/lib/i18n/strings_pt.g.dart index 9dc66a9f..0f5c9ed3 100644 --- a/lib/i18n/strings_pt.g.dart +++ b/lib/i18n/strings_pt.g.dart @@ -330,6 +330,12 @@ class _TranslationsSettingsPt implements TranslationsSettingsEn { @override String get autoPipDescription => 'Entrar automaticamente em picture-in-picture ao sair do app durante a reprodução'; @override String get matchContentFrameRate => 'Corresponder Taxa de Quadros do Conteúdo'; @override String get matchContentFrameRateDescription => 'Ajustar a taxa de atualização da tela para corresponder ao conteúdo de vídeo, reduzindo tremulação e economizando bateria'; + @override String get matchRefreshRate => 'Corresponder Taxa de Atualização'; + @override String get matchRefreshRateDescription => 'Alterar a taxa de atualização da tela para corresponder ao conteúdo de vídeo em tela cheia'; + @override String get matchDynamicRange => 'Corresponder Faixa Dinâmica'; + @override String get matchDynamicRangeDescription => 'Ativar automaticamente o HDR para conteúdo HDR e voltar ao SDR ao sair do reprodutor'; + @override String get displaySwitchDelay => 'Atraso na Troca de Tela'; + @override String get displaySwitchDelayDescription => 'Segundos de espera após uma troca de tela antes de iniciar a reprodução'; @override String get tunneledPlayback => 'Reprodução Tunelizada'; @override String get tunneledPlaybackDescription => 'Usar tunelamento de vídeo acelerado por hardware. Desative se você vir uma tela preta com áudio em conteúdo HDR'; @override String get requireProfileSelectionOnOpen => 'Pedir perfil ao abrir o app'; @@ -1537,6 +1543,12 @@ extension on TranslationsPt { 'settings.autoPipDescription' => 'Entrar automaticamente em picture-in-picture ao sair do app durante a reprodução', 'settings.matchContentFrameRate' => 'Corresponder Taxa de Quadros do Conteúdo', 'settings.matchContentFrameRateDescription' => 'Ajustar a taxa de atualização da tela para corresponder ao conteúdo de vídeo, reduzindo tremulação e economizando bateria', + 'settings.matchRefreshRate' => 'Corresponder Taxa de Atualização', + 'settings.matchRefreshRateDescription' => 'Alterar a taxa de atualização da tela para corresponder ao conteúdo de vídeo em tela cheia', + 'settings.matchDynamicRange' => 'Corresponder Faixa Dinâmica', + 'settings.matchDynamicRangeDescription' => 'Ativar automaticamente o HDR para conteúdo HDR e voltar ao SDR ao sair do reprodutor', + 'settings.displaySwitchDelay' => 'Atraso na Troca de Tela', + 'settings.displaySwitchDelayDescription' => 'Segundos de espera após uma troca de tela antes de iniciar a reprodução', 'settings.tunneledPlayback' => 'Reprodução Tunelizada', 'settings.tunneledPlaybackDescription' => 'Usar tunelamento de vídeo acelerado por hardware. Desative se você vir uma tela preta com áudio em conteúdo HDR', 'settings.requireProfileSelectionOnOpen' => 'Pedir perfil ao abrir o app', @@ -1842,14 +1854,14 @@ extension on TranslationsPt { 'hubDetail.dateAdded' => 'Data de Adição', 'hubDetail.rating' => 'Avaliação', 'hubDetail.noItemsFound' => 'Nenhum item encontrado', + _ => null, + } ?? switch (path) { 'logs.clearLogs' => 'Limpar Logs', 'logs.copyLogs' => 'Copiar Logs', 'logs.uploadLogs' => 'Enviar Logs', 'licenses.relatedPackages' => 'Pacotes Relacionados', 'licenses.license' => 'Licença', 'licenses.licenseNumber' => ({required Object number}) => 'Licença ${number}', - _ => null, - } ?? switch (path) { 'licenses.licensesCount' => ({required Object count}) => '${count} licenças', 'navigation.libraries' => 'Bibliotecas', 'navigation.downloads' => 'Downloads', diff --git a/lib/i18n/strings_ru.g.dart b/lib/i18n/strings_ru.g.dart index ee7a2a23..d2736eeb 100644 --- a/lib/i18n/strings_ru.g.dart +++ b/lib/i18n/strings_ru.g.dart @@ -330,6 +330,12 @@ class _TranslationsSettingsRu implements TranslationsSettingsEn { @override String get autoPipDescription => 'Автоматически переходить в режим «картинка в картинке» при выходе из приложения во время воспроизведения'; @override String get matchContentFrameRate => 'Соответствие частоты кадров контента'; @override String get matchContentFrameRateDescription => 'Настроить частоту обновления дисплея под видеоконтент, уменьшая дрожание и экономя батарею'; + @override String get matchRefreshRate => 'Соответствие частоты обновления'; + @override String get matchRefreshRateDescription => 'Переключать частоту обновления дисплея под видеоконтент в полноэкранном режиме'; + @override String get matchDynamicRange => 'Соответствие динамического диапазона'; + @override String get matchDynamicRangeDescription => 'Автоматически включать HDR для HDR-контента и возвращать SDR при выходе из плеера'; + @override String get displaySwitchDelay => 'Задержка переключения дисплея'; + @override String get displaySwitchDelayDescription => 'Секунды ожидания после смены режима дисплея перед началом воспроизведения'; @override String get tunneledPlayback => 'Туннельное воспроизведение'; @override String get tunneledPlaybackDescription => 'Использовать аппаратный видеотуннелинг. Отключите, если видите чёрный экран со звуком при HDR-контенте'; @override String get requireProfileSelectionOnOpen => 'Запрашивать профиль при запуске'; @@ -1537,6 +1543,12 @@ extension on TranslationsRu { 'settings.autoPipDescription' => 'Автоматически переходить в режим «картинка в картинке» при выходе из приложения во время воспроизведения', 'settings.matchContentFrameRate' => 'Соответствие частоты кадров контента', 'settings.matchContentFrameRateDescription' => 'Настроить частоту обновления дисплея под видеоконтент, уменьшая дрожание и экономя батарею', + 'settings.matchRefreshRate' => 'Соответствие частоты обновления', + 'settings.matchRefreshRateDescription' => 'Переключать частоту обновления дисплея под видеоконтент в полноэкранном режиме', + 'settings.matchDynamicRange' => 'Соответствие динамического диапазона', + 'settings.matchDynamicRangeDescription' => 'Автоматически включать HDR для HDR-контента и возвращать SDR при выходе из плеера', + 'settings.displaySwitchDelay' => 'Задержка переключения дисплея', + 'settings.displaySwitchDelayDescription' => 'Секунды ожидания после смены режима дисплея перед началом воспроизведения', 'settings.tunneledPlayback' => 'Туннельное воспроизведение', 'settings.tunneledPlaybackDescription' => 'Использовать аппаратный видеотуннелинг. Отключите, если видите чёрный экран со звуком при HDR-контенте', 'settings.requireProfileSelectionOnOpen' => 'Запрашивать профиль при запуске', @@ -1842,14 +1854,14 @@ extension on TranslationsRu { 'hubDetail.dateAdded' => 'Дата добавления', 'hubDetail.rating' => 'Рейтинг', 'hubDetail.noItemsFound' => 'Элементы не найдены', + _ => null, + } ?? switch (path) { 'logs.clearLogs' => 'Очистить логи', 'logs.copyLogs' => 'Скопировать логи', 'logs.uploadLogs' => 'Загрузить логи', 'licenses.relatedPackages' => 'Связанные пакеты', 'licenses.license' => 'Лицензия', 'licenses.licenseNumber' => ({required Object number}) => 'Лицензия ${number}', - _ => null, - } ?? switch (path) { 'licenses.licensesCount' => ({required Object count}) => '${count} лицензий', 'navigation.libraries' => 'Библиотеки', 'navigation.downloads' => 'Загрузки', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index 6702383d..28c05326 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -330,6 +330,12 @@ class _TranslationsSettingsSv implements TranslationsSettingsEn { @override String get autoPipDescription => 'Aktivera bild-i-bild automatiskt när appen lämnas under uppspelning'; @override String get matchContentFrameRate => 'Matcha innehållets bildfrekvens'; @override String get matchContentFrameRateDescription => 'Justera skärmens uppdateringsfrekvens för att matcha videoinnehållet, minskar hackighet och sparar batteri'; + @override String get matchRefreshRate => 'Matcha uppdateringsfrekvens'; + @override String get matchRefreshRateDescription => 'Byt skärmens uppdateringsfrekvens för att matcha videoinnehåll i helskärm'; + @override String get matchDynamicRange => 'Matcha dynamiskt omfång'; + @override String get matchDynamicRangeDescription => 'Aktivera HDR automatiskt för HDR-innehåll och återgå till SDR när spelaren stängs'; + @override String get displaySwitchDelay => 'Fördröjning vid skärmbyte'; + @override String get displaySwitchDelayDescription => 'Sekunder att vänta efter ett skärmlägesbyte innan uppspelning startar'; @override String get tunneledPlayback => 'Tunneluppspelning'; @override String get tunneledPlaybackDescription => 'Använd hårdvaruaccelererad videotunnling. Inaktivera om du ser en svart skärm med ljud vid HDR-innehåll'; @override String get requireProfileSelectionOnOpen => 'Fråga efter profil vid appstart'; @@ -1537,6 +1543,12 @@ extension on TranslationsSv { 'settings.autoPipDescription' => 'Aktivera bild-i-bild automatiskt när appen lämnas under uppspelning', 'settings.matchContentFrameRate' => 'Matcha innehållets bildfrekvens', 'settings.matchContentFrameRateDescription' => 'Justera skärmens uppdateringsfrekvens för att matcha videoinnehållet, minskar hackighet och sparar batteri', + 'settings.matchRefreshRate' => 'Matcha uppdateringsfrekvens', + 'settings.matchRefreshRateDescription' => 'Byt skärmens uppdateringsfrekvens för att matcha videoinnehåll i helskärm', + 'settings.matchDynamicRange' => 'Matcha dynamiskt omfång', + 'settings.matchDynamicRangeDescription' => 'Aktivera HDR automatiskt för HDR-innehåll och återgå till SDR när spelaren stängs', + 'settings.displaySwitchDelay' => 'Fördröjning vid skärmbyte', + 'settings.displaySwitchDelayDescription' => 'Sekunder att vänta efter ett skärmlägesbyte innan uppspelning startar', 'settings.tunneledPlayback' => 'Tunneluppspelning', 'settings.tunneledPlaybackDescription' => 'Använd hårdvaruaccelererad videotunnling. Inaktivera om du ser en svart skärm med ljud vid HDR-innehåll', 'settings.requireProfileSelectionOnOpen' => 'Fråga efter profil vid appstart', @@ -1842,14 +1854,14 @@ extension on TranslationsSv { 'hubDetail.dateAdded' => 'Datum tillagd', 'hubDetail.rating' => 'Betyg', 'hubDetail.noItemsFound' => 'Inga objekt hittades', + _ => null, + } ?? switch (path) { 'logs.clearLogs' => 'Rensa loggar', 'logs.copyLogs' => 'Kopiera loggar', 'logs.uploadLogs' => 'Ladda upp loggar', 'licenses.relatedPackages' => 'Relaterade paket', 'licenses.license' => 'Licens', 'licenses.licenseNumber' => ({required Object number}) => 'Licens ${number}', - _ => null, - } ?? switch (path) { 'licenses.licensesCount' => ({required Object count}) => '${count} licenser', 'navigation.libraries' => 'Bibliotek', 'navigation.downloads' => 'Nerladdat', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index 01cdfc7c..2420c5cb 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -330,6 +330,12 @@ class _TranslationsSettingsZh implements TranslationsSettingsEn { @override String get autoPipDescription => '在播放期间离开应用时自动进入画中画模式'; @override String get matchContentFrameRate => '匹配内容帧率'; @override String get matchContentFrameRateDescription => '调整显示刷新率以匹配视频内容,减少画面抖动并节省电量'; + @override String get matchRefreshRate => '匹配刷新率'; + @override String get matchRefreshRateDescription => '全屏时切换显示刷新率以匹配视频内容'; + @override String get matchDynamicRange => '匹配动态范围'; + @override String get matchDynamicRangeDescription => '自动为HDR内容启用HDR,退出播放器时恢复为SDR'; + @override String get displaySwitchDelay => '显示切换延迟'; + @override String get displaySwitchDelayDescription => '显示模式更改后等待多少秒再开始播放'; @override String get tunneledPlayback => '通道化播放'; @override String get tunneledPlaybackDescription => '使用硬件加速视频通道。如果在 HDR 内容上看到黑屏但有声音,请禁用此选项'; @override String get requireProfileSelectionOnOpen => '打开应用时询问配置文件'; @@ -1537,6 +1543,12 @@ extension on TranslationsZh { 'settings.autoPipDescription' => '在播放期间离开应用时自动进入画中画模式', 'settings.matchContentFrameRate' => '匹配内容帧率', 'settings.matchContentFrameRateDescription' => '调整显示刷新率以匹配视频内容,减少画面抖动并节省电量', + 'settings.matchRefreshRate' => '匹配刷新率', + 'settings.matchRefreshRateDescription' => '全屏时切换显示刷新率以匹配视频内容', + 'settings.matchDynamicRange' => '匹配动态范围', + 'settings.matchDynamicRangeDescription' => '自动为HDR内容启用HDR,退出播放器时恢复为SDR', + 'settings.displaySwitchDelay' => '显示切换延迟', + 'settings.displaySwitchDelayDescription' => '显示模式更改后等待多少秒再开始播放', 'settings.tunneledPlayback' => '通道化播放', 'settings.tunneledPlaybackDescription' => '使用硬件加速视频通道。如果在 HDR 内容上看到黑屏但有声音,请禁用此选项', 'settings.requireProfileSelectionOnOpen' => '打开应用时询问配置文件', @@ -1842,14 +1854,14 @@ extension on TranslationsZh { 'hubDetail.dateAdded' => '添加日期', 'hubDetail.rating' => '评分', 'hubDetail.noItemsFound' => '未找到项目', + _ => null, + } ?? switch (path) { 'logs.clearLogs' => '清除日志', 'logs.copyLogs' => '复制日志', 'logs.uploadLogs' => '上传日志', 'licenses.relatedPackages' => '相关软件包', 'licenses.license' => '许可证', 'licenses.licenseNumber' => ({required Object number}) => '许可证 ${number}', - _ => null, - } ?? switch (path) { 'licenses.licensesCount' => ({required Object count}) => '${count} 个许可证', 'navigation.libraries' => '媒体库', 'navigation.downloads' => '下载', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 81e36d85..b0f3f2b3 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -211,6 +211,12 @@ "autoPipDescription": "Aktivera bild-i-bild automatiskt när appen lämnas under uppspelning", "matchContentFrameRate": "Matcha innehållets bildfrekvens", "matchContentFrameRateDescription": "Justera skärmens uppdateringsfrekvens för att matcha videoinnehållet, minskar hackighet och sparar batteri", + "matchRefreshRate": "Matcha uppdateringsfrekvens", + "matchRefreshRateDescription": "Byt skärmens uppdateringsfrekvens för att matcha videoinnehåll i helskärm", + "matchDynamicRange": "Matcha dynamiskt omfång", + "matchDynamicRangeDescription": "Aktivera HDR automatiskt för HDR-innehåll och återgå till SDR när spelaren stängs", + "displaySwitchDelay": "Fördröjning vid skärmbyte", + "displaySwitchDelayDescription": "Sekunder att vänta efter ett skärmlägesbyte innan uppspelning startar", "tunneledPlayback": "Tunneluppspelning", "tunneledPlaybackDescription": "Använd hårdvaruaccelererad videotunnling. Inaktivera om du ser en svart skärm med ljud vid HDR-innehåll", "requireProfileSelectionOnOpen": "Fråga efter profil vid appstart", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index 2c337daf..4141d4e2 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -211,6 +211,12 @@ "autoPipDescription": "在播放期间离开应用时自动进入画中画模式", "matchContentFrameRate": "匹配内容帧率", "matchContentFrameRateDescription": "调整显示刷新率以匹配视频内容,减少画面抖动并节省电量", + "matchRefreshRate": "匹配刷新率", + "matchRefreshRateDescription": "全屏时切换显示刷新率以匹配视频内容", + "matchDynamicRange": "匹配动态范围", + "matchDynamicRangeDescription": "自动为HDR内容启用HDR,退出播放器时恢复为SDR", + "displaySwitchDelay": "显示切换延迟", + "displaySwitchDelayDescription": "显示模式更改后等待多少秒再开始播放", "tunneledPlayback": "通道化播放", "tunneledPlaybackDescription": "使用硬件加速视频通道。如果在 HDR 内容上看到黑屏但有声音,请禁用此选项", "requireProfileSelectionOnOpen": "打开应用时询问配置文件", diff --git a/lib/screens/settings/playback_settings_screen.dart b/lib/screens/settings/playback_settings_screen.dart index dee0e8c5..228e6f5d 100644 --- a/lib/screens/settings/playback_settings_screen.dart +++ b/lib/screens/settings/playback_settings_screen.dart @@ -47,6 +47,9 @@ class _PlaybackSettingsScreenState extends State { bool _enableDiscordRPC = false; bool _autoPip = true; bool _matchContentFrameRate = false; + bool _matchRefreshRate = false; + bool _matchDynamicRange = false; + int _displaySwitchDelay = 0; bool _tunneledPlayback = true; bool _useExoPlayer = true; bool _useExternalPlayer = false; @@ -83,6 +86,9 @@ class _PlaybackSettingsScreenState extends State { _enableDiscordRPC = _settingsService.getEnableDiscordRPC(); _autoPip = _settingsService.getAutoPip(); _matchContentFrameRate = _settingsService.getMatchContentFrameRate(); + _matchRefreshRate = _settingsService.getMatchRefreshRate(); + _matchDynamicRange = _settingsService.getMatchDynamicRange(); + _displaySwitchDelay = _settingsService.getDisplaySwitchDelay(); _tunneledPlayback = _settingsService.getTunneledPlayback(); _useExoPlayer = _settingsService.getUseExoPlayer(); _useExternalPlayer = _settingsService.getUseExternalPlayer(); @@ -114,6 +120,9 @@ class _PlaybackSettingsScreenState extends State { _buildHardwareDecoding(), if ((Platform.isAndroid && !PlatformDetector.isTV()) || Platform.isIOS || Platform.isMacOS) _buildAutoPip(), if (Platform.isAndroid) _buildMatchContentFrameRate(), + if (Platform.isWindows) _buildMatchRefreshRate(), + if (Platform.isWindows) _buildMatchDynamicRange(), + if (Platform.isWindows && (_matchRefreshRate || _matchDynamicRange)) _buildDisplaySwitchDelay(), if (Platform.isAndroid && _useExoPlayer) _buildTunneledPlayback(), _buildBufferSizeSelector(), @@ -225,6 +234,54 @@ class _PlaybackSettingsScreenState extends State { ); } + Widget _buildMatchRefreshRate() { + return SwitchListTile( + secondary: const AppIcon(Symbols.display_settings_rounded, fill: 1), + title: Text(t.settings.matchRefreshRate), + subtitle: Text(t.settings.matchRefreshRateDescription), + value: _matchRefreshRate, + onChanged: (value) async { + setState(() => _matchRefreshRate = value); + await _settingsService.setMatchRefreshRate(value); + }, + ); + } + + Widget _buildMatchDynamicRange() { + return SwitchListTile( + secondary: const AppIcon(Symbols.hdr_on_rounded, fill: 1), + title: Text(t.settings.matchDynamicRange), + subtitle: Text(t.settings.matchDynamicRangeDescription), + value: _matchDynamicRange, + onChanged: (value) async { + setState(() => _matchDynamicRange = value); + await _settingsService.setMatchDynamicRange(value); + }, + ); + } + + Widget _buildDisplaySwitchDelay() { + return ListTile( + leading: const AppIcon(Symbols.timer_rounded, fill: 1), + title: Text(t.settings.displaySwitchDelay), + subtitle: Text(t.settings.secondsUnit(seconds: _displaySwitchDelay.toString())), + trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), + onTap: () => showNumericInputDialog( + context: context, + title: t.settings.displaySwitchDelay, + labelText: t.settings.secondsLabel, + suffixText: t.settings.secondsShort, + min: 0, + max: 10, + currentValue: _displaySwitchDelay, + onSave: (value) async { + setState(() => _displaySwitchDelay = value); + await _settingsService.setDisplaySwitchDelay(value); + }, + ), + ); + } + Widget _buildTunneledPlayback() { return SwitchListTile( secondary: const AppIcon(Symbols.tv_options_input_settings_rounded, fill: 1), diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 643810dc..1bb6a673 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -41,6 +41,7 @@ import '../services/media_controls_manager.dart'; import '../services/playback_initialization_service.dart'; import '../services/playback_progress_tracker.dart'; import '../services/offline_watch_sync_service.dart'; +import '../services/display_mode_service.dart'; import '../services/settings_service.dart'; import '../services/sleep_timer_service.dart'; import '../services/track_manager.dart'; @@ -450,6 +451,12 @@ class VideoPlayerScreenState extends State with WidgetsBindin final debugLoggingEnabled = settingsService.getEnableDebugLogging(); final useExoPlayer = settingsService.getUseExoPlayer(); + // Initialize Windows display mode service. + if (Platform.isWindows) { + _displayModeService = DisplayModeService(settingsService, FullscreenStateManager()); + FullscreenStateManager().addListener(_onFullscreenChanged); + } + // Create player (on Android, uses ExoPlayer by default, MPV as fallback) player = Player(useExoPlayer: useExoPlayer); @@ -670,6 +677,11 @@ class VideoPlayerScreenState extends State with WidgetsBindin if (Platform.isAndroid && settingsService.getMatchContentFrameRate()) { await _applyFrameRateMatching(); } + + // Apply Windows display mode matching (refresh rate, HDR) + if (Platform.isWindows && _displayModeService != null) { + await _applyWindowsDisplayMatching(); + } } _trackManager?.onPlaybackRestart(); }); @@ -715,6 +727,9 @@ class VideoPlayerScreenState extends State with WidgetsBindin } } + /// Windows display mode matching service. + DisplayModeService? _displayModeService; + /// Apply frame rate matching on Android by setting the display refresh rate /// to match the video content's frame rate. int _frameRateRetries = 0; @@ -766,6 +781,56 @@ class VideoPlayerScreenState extends State with WidgetsBindin } } + /// Apply Windows display mode matching (refresh rate, HDR). + Future _applyWindowsDisplayMatching() async { + if (player == null || _displayModeService == null) return; + + try { + final fpsStr = await player!.getProperty('container-fps'); + final fps = double.tryParse(fpsStr ?? ''); + + final sigPeakStr = await player!.getProperty('video-params/sig-peak'); + final sigPeak = double.tryParse(sigPeakStr ?? ''); + + final delay = await _displayModeService!.applyDisplayMatching( + fps: fps, + sigPeak: sigPeak, + ); + + if (delay > Duration.zero) { + await Future.delayed(delay); + } + } catch (e) { + appLogger.w('Failed to apply display mode matching', error: e); + } + } + + /// Called when fullscreen state changes — restore display mode if exiting fullscreen. + void _onFullscreenChanged() { + if (!FullscreenStateManager().isFullscreen && + _displayModeService != null && + _displayModeService!.anyChangeApplied) { + _restoreWindowsDisplayMode(); + } + } + + /// Restore Windows display mode to original state. + Future _restoreWindowsDisplayMode() async { + if (_displayModeService == null || !_displayModeService!.anyChangeApplied) return; + + try { + // If HDR was toggled, release mpv's HDR swapchain first. + if (_displayModeService!.hdrStateChanged && player != null) { + await player!.setProperty('target-colorspace-hint', 'no'); + await Future.delayed(const Duration(milliseconds: 200)); + } + + await _displayModeService!.restoreAll(); + } catch (e) { + appLogger.w('Failed to restore display mode', error: e); + } + } + /// Initialize the service layer Future _initializeServices() async { if (!mounted || player == null) return; @@ -1889,6 +1954,17 @@ class VideoPlayerScreenState extends State with WidgetsBindin // Clear Discord Rich Presence DiscordRPCService.instance.stopPlayback(); + // Clean up Windows display mode service + if (Platform.isWindows && _displayModeService != null) { + FullscreenStateManager().removeListener(_onFullscreenChanged); + } + if (Platform.isWindows && _displayModeService != null && _displayModeService!.anyChangeApplied) { + if (_displayModeService!.hdrStateChanged && player != null) { + player!.setProperty('target-colorspace-hint', 'no'); + } + _displayModeService!.restoreAll(); + } + // Clear frame rate matching and abandon audio focus before disposing player (Android only) if (Platform.isAndroid && player != null) { player!.clearVideoFrameRate(); @@ -2587,6 +2663,8 @@ class VideoPlayerScreenState extends State with WidgetsBindin _progressTracker?.stopTracking(); // Clear frame rate matching before disposing (Android only) await _clearFrameRateMatching(); + // Restore Windows display mode before disposing + await _restoreWindowsDisplayMode(); await player?.dispose(); } catch (e) { appLogger.d('Error disposing player before navigation', error: e); diff --git a/lib/services/display_mode_service.dart b/lib/services/display_mode_service.dart new file mode 100644 index 00000000..28f1b91e --- /dev/null +++ b/lib/services/display_mode_service.dart @@ -0,0 +1,185 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; + +import 'fullscreen_state_manager.dart'; +import 'settings_service.dart'; +import '../utils/app_logger.dart'; + +/// Orchestrates Windows display mode matching (refresh rate, HDR) during video playback. +/// Uses the same platform channel as the mpv player (com.plezy/mpv_player). +class DisplayModeService { + static const _channel = MethodChannel('com.plezy/mpv_player'); + + final SettingsService _settings; + final FullscreenStateManager _fullscreen; + + bool _displayModeChanged = false; + bool _hdrStateChanged = false; + + bool get displayModeChanged => _displayModeChanged; + bool get hdrStateChanged => _hdrStateChanged; + bool get anyChangeApplied => _displayModeChanged || _hdrStateChanged; + + DisplayModeService(this._settings, this._fullscreen); + + /// Apply display matching based on video properties. Returns the delay + /// duration to wait before starting playback. + Future applyDisplayMatching({ + required double? fps, + required double? sigPeak, + }) async { + if (!Platform.isWindows) return Duration.zero; + if (!_fullscreen.isFullscreen) return Duration.zero; + + bool anyChange = false; + + // Refresh rate matching. + if (_settings.getMatchRefreshRate() && fps != null && fps > 0) { + try { + final success = await _matchRefreshRate(fps); + anyChange |= success; + } catch (e) { + appLogger.w('Failed to match refresh rate', error: e); + } + } + + // Dynamic range matching. + if (_settings.getMatchDynamicRange() && sigPeak != null && sigPeak > 1.0) { + try { + final success = await _enableSystemHDR(); + anyChange |= success; + } catch (e) { + appLogger.w('Failed to enable system HDR', error: e); + } + } + + if (anyChange) { + final delaySec = _settings.getDisplaySwitchDelay(); + return Duration(seconds: delaySec); + } + + return Duration.zero; + } + + /// Restore all display settings to their original state. + Future restoreAll() async { + if (!Platform.isWindows) return; + + if (_hdrStateChanged) { + try { + await _channel.invokeMethod('restoreSystemHDR'); + _hdrStateChanged = false; + appLogger.d('Restored system HDR state'); + } catch (e) { + appLogger.w('Failed to restore system HDR', error: e); + } + } + + if (_displayModeChanged) { + try { + await _channel.invokeMethod('restoreDisplayMode'); + _displayModeChanged = false; + appLogger.d('Restored display mode'); + } catch (e) { + appLogger.w('Failed to restore display mode', error: e); + } + } + } + + Future _matchRefreshRate(double fps) async { + final currentMode = await _channel.invokeMapMethod('getCurrentDisplayMode'); + if (currentMode == null) return false; + + final currentWidth = currentMode['width'] as int; + final currentHeight = currentMode['height'] as int; + final currentRate = currentMode['refreshRate'] as int; + + // Find best matching rate from available modes. + final modes = await _channel.invokeListMethod('getDisplayModes'); + if (modes == null || modes.isEmpty) return false; + + final bestRate = _findBestRefreshRate(fps, modes, currentWidth, currentHeight); + if (bestRate == 0 || bestRate == currentRate) return false; + + final success = await _channel.invokeMethod('setDisplayMode', { + 'width': currentWidth, + 'height': currentHeight, + 'refreshRate': bestRate, + }); + + if (success == true) { + _displayModeChanged = true; + appLogger.d('Matched refresh rate: ${fps}fps -> ${bestRate}Hz'); + return true; + } + return false; + } + + Future _enableSystemHDR() async { + final supported = await _channel.invokeMethod('isHDRSupported'); + if (supported != true) return false; + + final alreadyEnabled = await _channel.invokeMethod('isHDREnabled'); + if (alreadyEnabled == true) return false; + + final success = await _channel.invokeMethod('setSystemHDR', { + 'enabled': true, + }); + + if (success == true) { + _hdrStateChanged = true; + appLogger.d('Enabled system HDR for HDR content'); + return true; + } + return false; + } + + /// Find the best matching refresh rate for a video fps. + /// Mirrors the C++ FindBestRefreshRate algorithm. + static int _findBestRefreshRate( + double videoFps, + List modes, + int currentWidth, + int currentHeight, + ) { + if (videoFps <= 0) return 0; + + // Collect unique refresh rates at current resolution. + final rates = {}; + for (final mode in modes) { + final w = mode['width'] as int; + final h = mode['height'] as int; + if (w == currentWidth && h == currentHeight) { + rates.add(mode['refreshRate'] as int); + } + } + + if (rates.isEmpty) return 0; + + int bestRate = 0; + int bestMultiplier = 0; + + for (final rate in rates) { + final ratio = rate / videoFps; + final rounded = ratio.roundToDouble(); + + if (rounded < 1.0) continue; + + final multiplier = rounded.toInt(); + final deviation = (ratio - rounded).abs() / rounded; + + // Within 0.5% tolerance. + if (deviation > 0.005) continue; + + if (bestRate == 0 || + multiplier < bestMultiplier || + (multiplier == bestMultiplier && rate > bestRate)) { + bestRate = rate; + bestMultiplier = multiplier; + } + } + + return bestRate; + } +} diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 99d13a3c..5abd87b6 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -100,6 +100,9 @@ class SettingsService extends BaseSharedPreferencesService { static const String _keyCustomShaderPresets = 'custom_shader_presets'; static const String _keyLiveTvDefaultFavorites = 'live_tv_default_favorites'; static const String _keyCustomRelayUrl = 'custom_relay_url'; + static const String _keyMatchRefreshRate = 'match_refresh_rate'; + static const String _keyMatchDynamicRange = 'match_dynamic_range'; + static const String _keyDisplaySwitchDelay = 'display_switch_delay'; SettingsService._(); @@ -1041,6 +1044,33 @@ class SettingsService extends BaseSharedPreferencesService { return prefs.getBool(_keyMatchContentFrameRate) ?? false; // Default disabled } + // Match Refresh Rate (Windows) + Future setMatchRefreshRate(bool enabled) async { + await prefs.setBool(_keyMatchRefreshRate, enabled); + } + + bool getMatchRefreshRate() { + return prefs.getBool(_keyMatchRefreshRate) ?? false; + } + + // Match Dynamic Range (Windows) + Future setMatchDynamicRange(bool enabled) async { + await prefs.setBool(_keyMatchDynamicRange, enabled); + } + + bool getMatchDynamicRange() { + return prefs.getBool(_keyMatchDynamicRange) ?? false; + } + + // Display Switch Delay (Windows, seconds) + Future setDisplaySwitchDelay(int seconds) async { + await prefs.setInt(_keyDisplaySwitchDelay, seconds.clamp(0, 10)); + } + + int getDisplaySwitchDelay() { + return prefs.getInt(_keyDisplaySwitchDelay) ?? 0; + } + // Tunneled Playback (Android ExoPlayer only) Future setTunneledPlayback(bool enabled) async { await prefs.setBool(_keyTunneledPlayback, enabled); diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt index 9b14c21d..a4afe274 100644 --- a/windows/runner/CMakeLists.txt +++ b/windows/runner/CMakeLists.txt @@ -14,6 +14,7 @@ add_executable(${BINARY_NAME} WIN32 "mpv/utils.cpp" "mpv/mpv_container.cpp" "mpv/mpv_core.cpp" + "mpv/display_mode_manager.cpp" "mpv/mpv_player.cpp" "mpv/mpv_plugin.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 43a8489b..c387d23b 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -3,6 +3,7 @@ #include #include "flutter_window.h" +#include "mpv/display_mode_manager.h" #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, @@ -45,6 +46,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, } window.SetQuitOnClose(true); + // Recover display mode if a prior crash left it changed. + mpv::DisplayModeManager::RecoverIfNeeded( + ::GetAncestor(window.GetHandle(), GA_ROOT)); + ::MSG msg; while (::GetMessage(&msg, nullptr, 0, 0)) { ::TranslateMessage(&msg); diff --git a/windows/runner/mpv/display_mode_manager.cpp b/windows/runner/mpv/display_mode_manager.cpp new file mode 100644 index 00000000..f0a3b1d5 --- /dev/null +++ b/windows/runner/mpv/display_mode_manager.cpp @@ -0,0 +1,640 @@ +#include "display_mode_manager.h" + +#include "sdk_26100.h" + +#include +#include + +namespace mpv { + +static const wchar_t* kRegistryPath = L"Software\\Plezy\\DisplayModeOverride"; +static const wchar_t* kRegDeviceName = L"DeviceName"; +static const wchar_t* kRegOriginalRefreshRate = L"OriginalRefreshRate"; +static const wchar_t* kRegOriginalWidth = L"OriginalWidth"; +static const wchar_t* kRegOriginalHeight = L"OriginalHeight"; +static const wchar_t* kRegOriginalHDR = L"OriginalHDREnabled"; +static const wchar_t* kRegModeChanged = L"ModeChanged"; +static const wchar_t* kRegHDRChanged = L"HDRChanged"; + +DisplayModeManager::DisplayModeManager() {} + +DisplayModeManager::~DisplayModeManager() {} + +// --- Monitor identification --- + +std::wstring DisplayModeManager::GetMonitorDeviceName(HWND window) { + HMONITOR monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONEAREST); + if (!monitor) return {}; + + MONITORINFOEXW mi = {}; + mi.cbSize = sizeof(mi); + if (!GetMonitorInfoW(monitor, &mi)) return {}; + + return mi.szDevice; +} + +std::vector DisplayModeManager::GetDisplayConfigPaths() { + UINT32 path_count = 0; + UINT32 mode_count = 0; + std::vector paths; + std::vector modes; + + constexpr UINT32 flags = QDC_ONLY_ACTIVE_PATHS; + LONG result; + + // Retry loop for ERROR_INSUFFICIENT_BUFFER (Kodi pattern). + do { + if (GetDisplayConfigBufferSizes(flags, &path_count, &mode_count) != ERROR_SUCCESS) + return {}; + + paths.resize(path_count); + modes.resize(mode_count); + + result = QueryDisplayConfig(flags, &path_count, paths.data(), + &mode_count, modes.data(), nullptr); + } while (result == ERROR_INSUFFICIENT_BUFFER); + + if (result != ERROR_SUCCESS) return {}; + + paths.resize(path_count); + return paths; +} + +std::optional DisplayModeManager::GetDisplayTargetId( + const std::wstring& gdi_device_name) { + // Follows Kodi's GetDisplayTargetId: iterate QueryDisplayConfig paths, + // match via DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME.viewGdiDeviceName. + DISPLAYCONFIG_SOURCE_DEVICE_NAME source = {}; + source.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; + source.header.size = sizeof(source); + + for (const auto& path : GetDisplayConfigPaths()) { + source.header.adapterId = path.sourceInfo.adapterId; + source.header.id = path.sourceInfo.id; + + if (DisplayConfigGetDeviceInfo(&source.header) == ERROR_SUCCESS && + gdi_device_name == source.viewGdiDeviceName) { + return DisplayConfigId{path.targetInfo.adapterId, path.targetInfo.id}; + } + } + return std::nullopt; +} + +bool DisplayModeManager::IsWin11_24H2OrNewer() { + // Win11 24H2 = build 26100+ + OSVERSIONINFOEXW osvi = {}; + osvi.dwOSVersionInfoSize = sizeof(osvi); + osvi.dwBuildNumber = 26100; + + DWORDLONG condition_mask = 0; + VER_SET_CONDITION(condition_mask, VER_BUILDNUMBER, VER_GREATER_EQUAL); + + return VerifyVersionInfoW(&osvi, VER_BUILDNUMBER, condition_mask) != FALSE; +} + +// --- Refresh rate / resolution --- + +std::vector DisplayModeManager::EnumerateDisplayModes(HWND window) { + std::wstring device_name = GetMonitorDeviceName(window); + if (device_name.empty()) return {}; + + std::vector modes; + DEVMODEW dm = {}; + dm.dmSize = sizeof(dm); + + for (DWORD i = 0; EnumDisplaySettingsW(device_name.c_str(), i, &dm); i++) { + DisplayMode mode; + mode.width = dm.dmPelsWidth; + mode.height = dm.dmPelsHeight; + mode.refresh_rate = dm.dmDisplayFrequency; + modes.push_back(mode); + } + + // Remove duplicates. + std::sort(modes.begin(), modes.end(), [](const DisplayMode& a, const DisplayMode& b) { + if (a.width != b.width) return a.width < b.width; + if (a.height != b.height) return a.height < b.height; + return a.refresh_rate < b.refresh_rate; + }); + modes.erase(std::unique(modes.begin(), modes.end(), [](const DisplayMode& a, const DisplayMode& b) { + return a.width == b.width && a.height == b.height && a.refresh_rate == b.refresh_rate; + }), modes.end()); + + return modes; +} + +DisplayMode DisplayModeManager::GetCurrentMode(HWND window) { + std::wstring device_name = GetMonitorDeviceName(window); + DisplayMode mode = {}; + + if (device_name.empty()) return mode; + + DEVMODEW dm = {}; + dm.dmSize = sizeof(dm); + if (EnumDisplaySettingsW(device_name.c_str(), ENUM_CURRENT_SETTINGS, &dm)) { + mode.width = dm.dmPelsWidth; + mode.height = dm.dmPelsHeight; + mode.refresh_rate = dm.dmDisplayFrequency; + } + return mode; +} + +void DisplayModeManager::SaveOriginalMode(HWND window) { + original_device_name_ = GetMonitorDeviceName(window); + if (original_device_name_.empty()) return; + + original_devmode_ = {}; + original_devmode_.dmSize = sizeof(original_devmode_); + EnumDisplaySettingsW(original_device_name_.c_str(), ENUM_CURRENT_SETTINGS, + &original_devmode_); +} + +bool DisplayModeManager::SetDisplayMode(HWND window, DWORD width, DWORD height, + DWORD refresh_rate) { + std::wstring device_name = GetMonitorDeviceName(window); + if (device_name.empty()) return false; + + // Save original mode if not already saved. + if (!mode_changed_) { + SaveOriginalMode(window); + } + + DEVMODEW dm = {}; + dm.dmSize = sizeof(dm); + dm.dmPelsWidth = width; + dm.dmPelsHeight = height; + dm.dmDisplayFrequency = refresh_rate; + dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY; + + bool changed = false; + + // Kodi's Win8+ workaround for exact integer refresh rates (24, 48, 60 Hz). + // Write desired mode to registry, apply from registry, restore registry. + // Source: xbmc/windowing/windows/WinSystemWin32.cpp:940-970. + if (refresh_rate == 24 || refresh_rate == 48 || refresh_rate == 60) { + DEVMODEW registry_dm = {}; + registry_dm.dmSize = sizeof(registry_dm); + if (EnumDisplaySettingsW(device_name.c_str(), ENUM_REGISTRY_SETTINGS, ®istry_dm)) { + LONG rc = ChangeDisplaySettingsExW(device_name.c_str(), &dm, nullptr, + CDS_UPDATEREGISTRY | CDS_NORESET, nullptr); + if (rc == DISP_CHANGE_SUCCESSFUL) { + rc = ChangeDisplaySettingsExW(device_name.c_str(), nullptr, nullptr, + CDS_FULLSCREEN, nullptr); + if (rc == DISP_CHANGE_SUCCESSFUL) changed = true; + + // Restore original registry settings. + registry_dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS; + ChangeDisplaySettingsExW(device_name.c_str(), ®istry_dm, nullptr, + CDS_UPDATEREGISTRY | CDS_NORESET, nullptr); + } + } + } + + // Standard path / fallback. + if (!changed) { + LONG rc = ChangeDisplaySettingsExW(device_name.c_str(), &dm, nullptr, + CDS_FULLSCREEN, nullptr); + if (rc == DISP_CHANGE_SUCCESSFUL) changed = true; + } + + if (changed) { + mode_changed_ = true; + WriteRecoveryState(); + } + + return changed; +} + +bool DisplayModeManager::RestoreOriginalMode(HWND window) { + if (!mode_changed_ || original_device_name_.empty()) return false; + + original_devmode_.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS; + + LONG rc = ChangeDisplaySettingsExW(original_device_name_.c_str(), + &original_devmode_, nullptr, + CDS_FULLSCREEN, nullptr); + + if (rc == DISP_CHANGE_SUCCESSFUL) { + mode_changed_ = false; + if (!hdr_changed_) ClearRecoveryState(); + return true; + } + + // Fallback: restore registry defaults. + rc = ChangeDisplaySettingsExW(original_device_name_.c_str(), nullptr, nullptr, 0, nullptr); + mode_changed_ = (rc != DISP_CHANGE_SUCCESSFUL); + if (!mode_changed_ && !hdr_changed_) ClearRecoveryState(); + return rc == DISP_CHANGE_SUCCESSFUL; +} + +// --- HDR --- + +bool DisplayModeManager::IsHDRSupported(HWND window) { + std::wstring device_name = GetMonitorDeviceName(window); + if (device_name.empty()) return false; + + auto target_id = GetDisplayTargetId(device_name); + if (!target_id) return false; + + // Follows Kodi's GetDisplayHDRStatus pattern. + if (IsWin11_24H2OrNewer()) { + DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO_2 info = {}; + info.header.type = static_cast( + DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO_2); + info.header.size = sizeof(info); + info.header.adapterId = target_id->adapter_id; + info.header.id = target_id->id; + + if (DisplayConfigGetDeviceInfo(&info.header) == ERROR_SUCCESS) { + return info.highDynamicRangeSupported == TRUE; + } + } else { + DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO info = {}; + info.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO; + info.header.size = sizeof(info); + info.header.adapterId = target_id->adapter_id; + info.header.id = target_id->id; + + if (DisplayConfigGetDeviceInfo(&info.header) == ERROR_SUCCESS) { + // advancedColorSupported=1 && wideColorEnforced=0 => true HDR screen. + // advancedColorSupported=1 && wideColorEnforced=1 => SDR screen with ACM (Win11 22H2+). + // Source: Kodi DisplayUtilsWin32.cpp:157-172. + return info.advancedColorSupported && !info.wideColorEnforced; + } + } + + return false; +} + +bool DisplayModeManager::IsHDREnabled(HWND window) { + std::wstring device_name = GetMonitorDeviceName(window); + if (device_name.empty()) return false; + + auto target_id = GetDisplayTargetId(device_name); + if (!target_id) return false; + + if (IsWin11_24H2OrNewer()) { + DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO_2 info = {}; + info.header.type = static_cast( + DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO_2); + info.header.size = sizeof(info); + info.header.adapterId = target_id->adapter_id; + info.header.id = target_id->id; + + if (DisplayConfigGetDeviceInfo(&info.header) == ERROR_SUCCESS) { + return info.activeColorMode == DISPLAYCONFIG_ADVANCED_COLOR_MODE_HDR; + } + } else { + DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO info = {}; + info.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO; + info.header.size = sizeof(info); + info.header.adapterId = target_id->adapter_id; + info.header.id = target_id->id; + + if (DisplayConfigGetDeviceInfo(&info.header) == ERROR_SUCCESS) { + bool hdr_supported = info.advancedColorSupported && !info.wideColorEnforced; + return hdr_supported && info.advancedColorEnabled; + } + } + + return false; +} + +void DisplayModeManager::SaveOriginalHDRState(HWND window) { + original_hdr_device_name_ = GetMonitorDeviceName(window); + original_hdr_enabled_ = IsHDREnabled(window); +} + +bool DisplayModeManager::SetHDREnabled(HWND window, bool enabled) { + std::wstring device_name = GetMonitorDeviceName(window); + if (device_name.empty()) return false; + + auto target_id = GetDisplayTargetId(device_name); + if (!target_id) return false; + + // Save original state if not already saved. + if (!hdr_changed_) { + SaveOriginalHDRState(window); + } + + // Save DEVMODEW before toggle — Windows changes display mode on HDR state change. + // Source: Kodi WIN32Util.cpp:1252-1257. + DEVMODEW pre_toggle_dm = {}; + pre_toggle_dm.dmSize = sizeof(pre_toggle_dm); + EnumDisplaySettingsW(device_name.c_str(), ENUM_CURRENT_SETTINGS, &pre_toggle_dm); + + // Toggle HDR. + LONG result; + if (IsWin11_24H2OrNewer()) { + DISPLAYCONFIG_SET_HDR_STATE state = {}; + state.header.type = static_cast( + DISPLAYCONFIG_DEVICE_INFO_SET_HDR_STATE); + state.header.size = sizeof(state); + state.header.adapterId = target_id->adapter_id; + state.header.id = target_id->id; + state.enableHdr = enabled ? TRUE : FALSE; + result = DisplayConfigSetDeviceInfo(&state.header); + } else { + DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE state = {}; + state.header.type = DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE; + state.header.size = sizeof(state); + state.header.adapterId = target_id->adapter_id; + state.header.id = target_id->id; + state.enableAdvancedColor = enabled ? TRUE : FALSE; + result = DisplayConfigSetDeviceInfo(&state.header); + } + + if (result != ERROR_SUCCESS) return false; + + // Restore DEVMODEW after toggle — Windows may have changed the display mode. + // Source: Kodi WIN32Util.cpp:1276-1288. + if (pre_toggle_dm.dmDisplayFrequency != 0) { + pre_toggle_dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS; + ChangeDisplaySettingsExW(device_name.c_str(), &pre_toggle_dm, nullptr, + CDS_FULLSCREEN, nullptr); + } + + hdr_changed_ = true; + WriteRecoveryState(); + return true; +} + +bool DisplayModeManager::RestoreOriginalHDRState(HWND window) { + if (!hdr_changed_ || original_hdr_device_name_.empty()) return false; + + bool current = IsHDREnabled(window); + if (current == original_hdr_enabled_) { + hdr_changed_ = false; + if (!mode_changed_) ClearRecoveryState(); + return true; + } + + // Need to actually toggle back. + auto target_id = GetDisplayTargetId(original_hdr_device_name_); + if (!target_id) return false; + + // Save DEVMODEW before restore toggle. + DEVMODEW pre_toggle_dm = {}; + pre_toggle_dm.dmSize = sizeof(pre_toggle_dm); + EnumDisplaySettingsW(original_hdr_device_name_.c_str(), ENUM_CURRENT_SETTINGS, &pre_toggle_dm); + + LONG result; + if (IsWin11_24H2OrNewer()) { + DISPLAYCONFIG_SET_HDR_STATE state = {}; + state.header.type = static_cast( + DISPLAYCONFIG_DEVICE_INFO_SET_HDR_STATE); + state.header.size = sizeof(state); + state.header.adapterId = target_id->adapter_id; + state.header.id = target_id->id; + state.enableHdr = original_hdr_enabled_ ? TRUE : FALSE; + result = DisplayConfigSetDeviceInfo(&state.header); + } else { + DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE state = {}; + state.header.type = DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE; + state.header.size = sizeof(state); + state.header.adapterId = target_id->adapter_id; + state.header.id = target_id->id; + state.enableAdvancedColor = original_hdr_enabled_ ? TRUE : FALSE; + result = DisplayConfigSetDeviceInfo(&state.header); + } + + if (result != ERROR_SUCCESS) return false; + + // Restore DEVMODEW after toggle. + if (pre_toggle_dm.dmDisplayFrequency != 0) { + pre_toggle_dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS; + ChangeDisplaySettingsExW(original_hdr_device_name_.c_str(), &pre_toggle_dm, + nullptr, CDS_FULLSCREEN, nullptr); + } + + hdr_changed_ = false; + if (!mode_changed_) ClearRecoveryState(); + return true; +} + +// --- Crash recovery (Windows Registry) --- + +bool DisplayModeManager::WriteRegistryDWORD(const wchar_t* value_name, DWORD value) { + HKEY key; + if (RegCreateKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, nullptr, + 0, KEY_WRITE, nullptr, &key, nullptr) != ERROR_SUCCESS) + return false; + LONG result = RegSetValueExW(key, value_name, 0, REG_DWORD, + reinterpret_cast(&value), sizeof(value)); + RegCloseKey(key); + return result == ERROR_SUCCESS; +} + +bool DisplayModeManager::WriteRegistryString(const wchar_t* value_name, + const std::wstring& value) { + HKEY key; + if (RegCreateKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, nullptr, + 0, KEY_WRITE, nullptr, &key, nullptr) != ERROR_SUCCESS) + return false; + LONG result = RegSetValueExW(key, value_name, 0, REG_SZ, + reinterpret_cast(value.c_str()), + static_cast((value.size() + 1) * sizeof(wchar_t))); + RegCloseKey(key); + return result == ERROR_SUCCESS; +} + +bool DisplayModeManager::ReadRegistryDWORD(const wchar_t* value_name, DWORD& value) { + HKEY key; + if (RegOpenKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, KEY_READ, &key) != ERROR_SUCCESS) + return false; + DWORD size = sizeof(value); + DWORD type = 0; + LONG result = RegQueryValueExW(key, value_name, nullptr, &type, + reinterpret_cast(&value), &size); + RegCloseKey(key); + return result == ERROR_SUCCESS && type == REG_DWORD; +} + +bool DisplayModeManager::ReadRegistryString(const wchar_t* value_name, std::wstring& value) { + HKEY key; + if (RegOpenKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, KEY_READ, &key) != ERROR_SUCCESS) + return false; + DWORD size = 0; + DWORD type = 0; + RegQueryValueExW(key, value_name, nullptr, &type, nullptr, &size); + if (type != REG_SZ || size == 0) { + RegCloseKey(key); + return false; + } + value.resize(size / sizeof(wchar_t)); + LONG result = RegQueryValueExW(key, value_name, nullptr, nullptr, + reinterpret_cast(&value[0]), &size); + RegCloseKey(key); + if (result != ERROR_SUCCESS) return false; + // Remove trailing null. + while (!value.empty() && value.back() == L'\0') value.pop_back(); + return true; +} + +bool DisplayModeManager::DeleteRegistryValue(const wchar_t* value_name) { + HKEY key; + if (RegOpenKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, KEY_WRITE, &key) != ERROR_SUCCESS) + return false; + RegDeleteValueW(key, value_name); + RegCloseKey(key); + return true; +} + +void DisplayModeManager::WriteRecoveryState() { + std::wstring device = mode_changed_ ? original_device_name_ : original_hdr_device_name_; + if (device.empty()) return; + + WriteRegistryString(kRegDeviceName, device); + WriteRegistryDWORD(kRegModeChanged, mode_changed_ ? 1 : 0); + WriteRegistryDWORD(kRegHDRChanged, hdr_changed_ ? 1 : 0); + + if (mode_changed_) { + WriteRegistryDWORD(kRegOriginalRefreshRate, original_devmode_.dmDisplayFrequency); + WriteRegistryDWORD(kRegOriginalWidth, original_devmode_.dmPelsWidth); + WriteRegistryDWORD(kRegOriginalHeight, original_devmode_.dmPelsHeight); + } + + if (hdr_changed_) { + WriteRegistryDWORD(kRegOriginalHDR, original_hdr_enabled_ ? 1 : 0); + } +} + +void DisplayModeManager::ClearRecoveryState() { + // Delete the entire key. + RegDeleteKeyW(HKEY_CURRENT_USER, kRegistryPath); +} + +bool DisplayModeManager::RecoverIfNeeded(HWND window) { + DWORD mode_changed = 0, hdr_changed = 0; + std::wstring device_name; + + if (!ReadRegistryString(kRegDeviceName, device_name)) return false; + ReadRegistryDWORD(kRegModeChanged, mode_changed); + ReadRegistryDWORD(kRegHDRChanged, hdr_changed); + + if (!mode_changed && !hdr_changed) { + RegDeleteKeyW(HKEY_CURRENT_USER, kRegistryPath); + return false; + } + + bool recovered = false; + + // Restore refresh rate / resolution. + if (mode_changed) { + DWORD width = 0, height = 0, refresh = 0; + ReadRegistryDWORD(kRegOriginalWidth, width); + ReadRegistryDWORD(kRegOriginalHeight, height); + ReadRegistryDWORD(kRegOriginalRefreshRate, refresh); + + if (width > 0 && height > 0 && refresh > 0) { + DEVMODEW dm = {}; + dm.dmSize = sizeof(dm); + dm.dmPelsWidth = width; + dm.dmPelsHeight = height; + dm.dmDisplayFrequency = refresh; + dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY; + + LONG rc = ChangeDisplaySettingsExW(device_name.c_str(), &dm, nullptr, + CDS_FULLSCREEN, nullptr); + if (rc == DISP_CHANGE_SUCCESSFUL) recovered = true; + } + } + + // Restore HDR state. + if (hdr_changed) { + DWORD hdr_was_enabled = 0; + ReadRegistryDWORD(kRegOriginalHDR, hdr_was_enabled); + + auto target_id = GetDisplayTargetId(device_name); + if (target_id) { + // Save DEVMODEW before toggle. + DEVMODEW pre_dm = {}; + pre_dm.dmSize = sizeof(pre_dm); + EnumDisplaySettingsW(device_name.c_str(), ENUM_CURRENT_SETTINGS, &pre_dm); + + LONG result; + if (IsWin11_24H2OrNewer()) { + DISPLAYCONFIG_SET_HDR_STATE state = {}; + state.header.type = static_cast( + DISPLAYCONFIG_DEVICE_INFO_SET_HDR_STATE); + state.header.size = sizeof(state); + state.header.adapterId = target_id->adapter_id; + state.header.id = target_id->id; + state.enableHdr = hdr_was_enabled ? TRUE : FALSE; + result = DisplayConfigSetDeviceInfo(&state.header); + } else { + DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE state = {}; + state.header.type = DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE; + state.header.size = sizeof(state); + state.header.adapterId = target_id->adapter_id; + state.header.id = target_id->id; + state.enableAdvancedColor = hdr_was_enabled ? TRUE : FALSE; + result = DisplayConfigSetDeviceInfo(&state.header); + } + + if (result == ERROR_SUCCESS) { + recovered = true; + // Restore display mode after HDR toggle. + if (pre_dm.dmDisplayFrequency != 0) { + pre_dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS; + ChangeDisplaySettingsExW(device_name.c_str(), &pre_dm, nullptr, + CDS_FULLSCREEN, nullptr); + } + } + } + } + + // Clean up registry regardless of success. + RegDeleteKeyW(HKEY_CURRENT_USER, kRegistryPath); + return recovered; +} + +// --- Refresh rate matching --- + +DWORD DisplayModeManager::FindBestRefreshRate(double video_fps, + const std::vector& modes, + DWORD current_width, + DWORD current_height) { + if (video_fps <= 0) return 0; + + // Collect unique refresh rates available at the current resolution. + std::vector rates; + for (const auto& mode : modes) { + if (mode.width == current_width && mode.height == current_height) { + if (std::find(rates.begin(), rates.end(), mode.refresh_rate) == rates.end()) { + rates.push_back(mode.refresh_rate); + } + } + } + + if (rates.empty()) return 0; + + DWORD best_rate = 0; + int best_multiplier = 0; + + for (DWORD rate : rates) { + double ratio = static_cast(rate) / video_fps; + double rounded = std::round(ratio); + + // Must be a positive integer multiple (1x, 2x, 3x, ...). + if (rounded < 1.0) continue; + + int multiplier = static_cast(rounded); + double deviation = std::abs(ratio - rounded) / rounded; + + // Within 0.5% tolerance (covers 23.976 -> 24Hz, 29.97 -> 30Hz, etc.). + if (deviation > 0.005) continue; + + // Prefer lowest multiplier (exact match > 2x > 3x > ...). + // Among equal multipliers, prefer higher rate (shouldn't happen, but safe). + if (best_rate == 0 || multiplier < best_multiplier || + (multiplier == best_multiplier && rate > best_rate)) { + best_rate = rate; + best_multiplier = multiplier; + } + } + + return best_rate; +} + +} // namespace mpv diff --git a/windows/runner/mpv/display_mode_manager.h b/windows/runner/mpv/display_mode_manager.h new file mode 100644 index 00000000..f32dbb19 --- /dev/null +++ b/windows/runner/mpv/display_mode_manager.h @@ -0,0 +1,139 @@ +#ifndef DISPLAY_MODE_MANAGER_H_ +#define DISPLAY_MODE_MANAGER_H_ + +#include + +#include +#include +#include +#include + +namespace mpv { + +struct DisplayMode { + DWORD width; + DWORD height; + DWORD refresh_rate; +}; + +// Identifiers for a display target in the DisplayConfig API. +struct DisplayConfigId { + LUID adapter_id; + UINT32 id; +}; + +// Manages Windows display mode switching (refresh rate, HDR) for video playback. +// Pure Win32 utility — no mpv or Flutter dependency. +// +// References: +// ChangeDisplaySettingsExW: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-changedisplaysettingsexw +// EnumDisplaySettingsW: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaysettingsw +// DisplayConfigGetDeviceInfo: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-displayconfiggetdeviceinfo +// DisplayConfigSetDeviceInfo: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-displayconfigsetdeviceinfo +// Kodi impl: xbmc/platform/win32/DisplayUtilsWin32.cpp, xbmc/platform/win32/WIN32Util.cpp +class DisplayModeManager { + public: + DisplayModeManager(); + ~DisplayModeManager(); + + // --- Refresh rate / resolution --- + + // Enumerate available display modes for the monitor containing the window. + std::vector EnumerateDisplayModes(HWND window); + + // Get the current display mode. + DisplayMode GetCurrentMode(HWND window); + + // Save the current mode for later restoration. + void SaveOriginalMode(HWND window); + + // Change the display mode (refresh rate and/or resolution). + // Uses CDS_FULLSCREEN flag. Implements Kodi's Win8+ workaround for 24/48/60Hz. + // Returns true on success. + bool SetDisplayMode(HWND window, DWORD width, DWORD height, DWORD refresh_rate); + + // Restore the previously saved display mode. + bool RestoreOriginalMode(HWND window); + + // Returns true if a mode change has been applied (and not yet restored). + bool IsModeChanged() const { return mode_changed_; } + + // --- HDR --- + + // Check if the display supports HDR (not just ACM/WCG). + // Uses advancedColorSupported && !wideColorEnforced (pre-24H2) + // or highDynamicRangeSupported (Win11 24H2+). + bool IsHDRSupported(HWND window); + + // Check if HDR is currently enabled. + bool IsHDREnabled(HWND window); + + // Save the current HDR state for later restoration. + void SaveOriginalHDRState(HWND window); + + // Enable or disable system HDR. + // Saves/restores DEVMODEW around the toggle (Windows changes display mode on HDR state change). + // Uses SET_HDR_STATE (type 16) on Win11 24H2+, SET_ADVANCED_COLOR_STATE (type 10) on older. + bool SetHDREnabled(HWND window, bool enabled); + + // Restore the previously saved HDR state. + bool RestoreOriginalHDRState(HWND window); + + // Returns true if an HDR state change has been applied (and not yet restored). + bool IsHDRChanged() const { return hdr_changed_; } + + // --- Crash recovery --- + + // Write current override state to registry for crash recovery. + void WriteRecoveryState(); + + // Clear the recovery state from registry. + void ClearRecoveryState(); + + // Check for and recover from a prior crash that left display settings changed. + // Should be called early in app startup. Returns true if recovery was performed. + static bool RecoverIfNeeded(HWND window); + + // --- Refresh rate matching --- + + // Find the best matching refresh rate for a given video fps from available modes. + // Returns 0 if no suitable match found. + static DWORD FindBestRefreshRate(double video_fps, + const std::vector& modes, + DWORD current_width, DWORD current_height); + + private: + // Get the GDI device name for the monitor containing the window. + static std::wstring GetMonitorDeviceName(HWND window); + + // Get the DisplayConfig target ID for a given GDI device name. + // Follows Kodi's GetDisplayTargetId pattern. + static std::optional GetDisplayTargetId(const std::wstring& gdi_device_name); + + // Get all active display config paths (with retry for ERROR_INSUFFICIENT_BUFFER). + static std::vector GetDisplayConfigPaths(); + + // Check if running on Win11 24H2 or newer. + static bool IsWin11_24H2OrNewer(); + + // Registry helpers for crash recovery. + static bool WriteRegistryDWORD(const wchar_t* value_name, DWORD value); + static bool WriteRegistryString(const wchar_t* value_name, const std::wstring& value); + static bool ReadRegistryDWORD(const wchar_t* value_name, DWORD& value); + static bool ReadRegistryString(const wchar_t* value_name, std::wstring& value); + static bool DeleteRegistryValue(const wchar_t* value_name); + + // Stored original mode for restoration. + std::wstring original_device_name_; + DEVMODEW original_devmode_ = {}; + bool mode_changed_ = false; + + // Stored original HDR state for restoration. + std::wstring original_hdr_device_name_; + bool original_hdr_enabled_ = false; + bool hdr_changed_ = false; +}; + +} // namespace mpv + +#endif // DISPLAY_MODE_MANAGER_H_ diff --git a/windows/runner/mpv/mpv_plugin.cpp b/windows/runner/mpv/mpv_plugin.cpp index 7aec4fe5..dc785e8d 100644 --- a/windows/runner/mpv/mpv_plugin.cpp +++ b/windows/runner/mpv/mpv_plugin.cpp @@ -369,6 +369,75 @@ void MpvPlayerPlugin::HandleMethodCall( } else if (method == "isInitialized") { bool initialized = player_ && player_->IsInitialized(); result->Success(flutter::EncodableValue(initialized)); + + // --- Display mode matching --- + } else if (method == "getDisplayModes") { + HWND hwnd = GetWindow(); + auto modes = display_mode_manager_.EnumerateDisplayModes(hwnd); + flutter::EncodableList list; + for (const auto& mode : modes) { + flutter::EncodableMap m; + m[flutter::EncodableValue("width")] = flutter::EncodableValue(static_cast(mode.width)); + m[flutter::EncodableValue("height")] = flutter::EncodableValue(static_cast(mode.height)); + m[flutter::EncodableValue("refreshRate")] = flutter::EncodableValue(static_cast(mode.refresh_rate)); + list.push_back(flutter::EncodableValue(m)); + } + result->Success(flutter::EncodableValue(list)); + } else if (method == "getCurrentDisplayMode") { + HWND hwnd = GetWindow(); + auto mode = display_mode_manager_.GetCurrentMode(hwnd); + flutter::EncodableMap m; + m[flutter::EncodableValue("width")] = flutter::EncodableValue(static_cast(mode.width)); + m[flutter::EncodableValue("height")] = flutter::EncodableValue(static_cast(mode.height)); + m[flutter::EncodableValue("refreshRate")] = flutter::EncodableValue(static_cast(mode.refresh_rate)); + result->Success(flutter::EncodableValue(m)); + } else if (method == "setDisplayMode") { + const auto* args = method_call.arguments(); + if (!args || !std::holds_alternative(*args)) { + result->Error("INVALID_ARGS", "Expected map argument"); + return; + } + const auto& map = std::get(*args); + auto get_int = [&map](const char* key) -> int { + auto it = map.find(flutter::EncodableValue(key)); + if (it != map.end() && std::holds_alternative(it->second)) + return std::get(it->second); + return 0; + }; + HWND hwnd = GetWindow(); + bool success = display_mode_manager_.SetDisplayMode( + hwnd, get_int("width"), get_int("height"), get_int("refreshRate")); + result->Success(flutter::EncodableValue(success)); + } else if (method == "restoreDisplayMode") { + HWND hwnd = GetWindow(); + bool success = display_mode_manager_.RestoreOriginalMode(hwnd); + result->Success(flutter::EncodableValue(success)); + } else if (method == "isHDRSupported") { + HWND hwnd = GetWindow(); + result->Success(flutter::EncodableValue(display_mode_manager_.IsHDRSupported(hwnd))); + } else if (method == "isHDREnabled") { + HWND hwnd = GetWindow(); + result->Success(flutter::EncodableValue(display_mode_manager_.IsHDREnabled(hwnd))); + } else if (method == "setSystemHDR") { + const auto* args = method_call.arguments(); + if (!args || !std::holds_alternative(*args)) { + result->Error("INVALID_ARGS", "Expected map argument"); + return; + } + const auto& map = std::get(*args); + auto it = map.find(flutter::EncodableValue("enabled")); + if (it == map.end() || !std::holds_alternative(it->second)) { + result->Error("INVALID_ARGS", "Missing 'enabled'"); + return; + } + bool enabled = std::get(it->second); + HWND hwnd = GetWindow(); + bool success = display_mode_manager_.SetHDREnabled(hwnd, enabled); + result->Success(flutter::EncodableValue(success)); + } else if (method == "restoreSystemHDR") { + HWND hwnd = GetWindow(); + bool success = display_mode_manager_.RestoreOriginalHDRState(hwnd); + result->Success(flutter::EncodableValue(success)); } else { result->NotImplemented(); } diff --git a/windows/runner/mpv/mpv_plugin.h b/windows/runner/mpv/mpv_plugin.h index 2f4d2f7a..1136fa06 100644 --- a/windows/runner/mpv/mpv_plugin.h +++ b/windows/runner/mpv/mpv_plugin.h @@ -11,6 +11,7 @@ #include #include +#include "display_mode_manager.h" #include "mpv_core.h" #include "mpv_player.h" @@ -45,6 +46,7 @@ class MpvPlayerPlugin : public flutter::Plugin { std::unique_ptr> event_sink_; std::unique_ptr player_; + DisplayModeManager display_mode_manager_; std::optional proc_id_; }; diff --git a/windows/runner/mpv/sdk_26100.h b/windows/runner/mpv/sdk_26100.h new file mode 100644 index 00000000..c9640aed --- /dev/null +++ b/windows/runner/mpv/sdk_26100.h @@ -0,0 +1,55 @@ +#ifndef SDK_26100_H_ +#define SDK_26100_H_ + +// Win11 24H2 (SDK 10.0.26100.0) struct shims for HDR APIs. +// These types may not be present in older Windows SDK versions. +// Based on Kodi's SDK_26100.h (xbmc/platform/win32/SDK_26100.h). + +#include + +enum { + DISPLAYCONFIG_DEVICE_INFO_SET_RESERVED1 = 14, + DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO_2 = 15, + DISPLAYCONFIG_DEVICE_INFO_SET_HDR_STATE = 16, + DISPLAYCONFIG_DEVICE_INFO_SET_WCG_STATE = 17, +}; + +typedef enum _DISPLAYCONFIG_ADVANCED_COLOR_MODE { + DISPLAYCONFIG_ADVANCED_COLOR_MODE_SDR, + DISPLAYCONFIG_ADVANCED_COLOR_MODE_WCG, + DISPLAYCONFIG_ADVANCED_COLOR_MODE_HDR +} DISPLAYCONFIG_ADVANCED_COLOR_MODE; + +typedef struct _DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO_2 { + DISPLAYCONFIG_DEVICE_INFO_HEADER header; + union { + struct { + UINT32 advancedColorSupported : 1; + UINT32 advancedColorActive : 1; + UINT32 reserved1 : 1; + UINT32 advancedColorLimitedByPolicy : 1; + UINT32 highDynamicRangeSupported : 1; + UINT32 highDynamicRangeUserEnabled : 1; + UINT32 wideColorSupported : 1; + UINT32 wideColorUserEnabled : 1; + UINT32 reserved : 24; + }; + UINT32 value; + }; + DISPLAYCONFIG_COLOR_ENCODING colorEncoding; + UINT32 bitsPerColorChannel; + DISPLAYCONFIG_ADVANCED_COLOR_MODE activeColorMode; +} DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO_2; + +typedef struct _DISPLAYCONFIG_SET_HDR_STATE { + DISPLAYCONFIG_DEVICE_INFO_HEADER header; + union { + struct { + UINT32 enableHdr : 1; + UINT32 reserved : 31; + }; + UINT32 value; + }; +} DISPLAYCONFIG_SET_HDR_STATE; + +#endif // SDK_26100_H_