From a97be5823f44554c7c20137cdfd67f5f50ffa173 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:42:38 +0200 Subject: [PATCH] feat(player): add play-next countdown, gesture toggles, and deinterlacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users could not adjust the five-second Play Next timer, switch off the edge-swipe/pinch gestures they trigger by accident, deinterlace DVD-era content, or keep always-on-top across episodes. Play Next Countdown (0-30 s) joins Auto-Play & Skip; zero skips the prompt and starts the next episode immediately, while the transient- retry prompt keeps its fixed five-second spacing. Brightness swipe, volume swipe, and pinch-to-zoom each get a switch in a mobile-only Gestures group; a disabled edge also stops stealing the content-strip drag. A Deinterlacing toggle (mpv-only — ExoPlayer has no filter chain) sets mpv's deinterlace=auto at player init. The player's always-on-top toggle now persists via a pref that is re-applied when the next player opens, including autoplay episode transitions; the window flag is still dropped when the player closes. Also resolves the reorg TODO markers per triage: hold-to-speed lock (#1046) will be gesture behavior rather than a setting, metadata language (#1668) and delete re-authentication (#1924) are rejected, and always-on-top is player-persisted state, not a Window setting (#1386). close #1827 close #1810 close #2149 close #931 --- lib/i18n/az.i18n.json | 11 +++ lib/i18n/bg.i18n.json | 11 +++ lib/i18n/da.i18n.json | 11 +++ lib/i18n/de.i18n.json | 11 +++ lib/i18n/en.i18n.json | 11 +++ lib/i18n/es.i18n.json | 11 +++ lib/i18n/fr.i18n.json | 11 +++ lib/i18n/hu.i18n.json | 11 +++ lib/i18n/it.i18n.json | 11 +++ lib/i18n/ja.i18n.json | 11 +++ lib/i18n/kk.i18n.json | 11 +++ lib/i18n/ko.i18n.json | 11 +++ lib/i18n/nb.i18n.json | 11 +++ lib/i18n/nl.i18n.json | 11 +++ lib/i18n/pl.i18n.json | 11 +++ lib/i18n/pt.i18n.json | 11 +++ lib/i18n/ru.i18n.json | 11 +++ lib/i18n/strings.g.dart | 2 +- lib/i18n/strings_en.g.dart | 56 ++++++++++-- lib/i18n/sv.i18n.json | 11 +++ lib/i18n/tr.i18n.json | 11 +++ lib/i18n/uz.i18n.json | 11 +++ lib/i18n/zh-Hant.i18n.json | 11 +++ lib/i18n/zh.i18n.json | 11 +++ .../settings/general_settings_screen.dart | 13 +-- .../settings/playback_settings_screen.dart | 87 ++++++++++++------ lib/screens/settings/settings_screen.dart | 2 - lib/screens/video_player/parts/build.dart | 2 + .../video_player/parts/playback_prompts.dart | 10 ++- lib/screens/video_player_screen.dart | 12 +++ lib/services/settings_service.dart | 27 ++++++ .../mobile_edge_adjustment_tracker.dart | 14 ++- .../video_controls/parts/playback_input.dart | 19 +++- .../video_controls/parts/visibility.dart | 11 ++- .../playback_settings_screen_test.dart | 89 +++++++++++++++++++ ...s_mobile_edge_adjustment_tracker_test.dart | 37 ++++++++ 36 files changed, 568 insertions(+), 55 deletions(-) diff --git a/lib/i18n/az.i18n.json b/lib/i18n/az.i18n.json index b7b69e3ba..910f12d29 100644 --- a/lib/i18n/az.i18n.json +++ b/lib/i18n/az.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "Girişi avtomatik ötür", "autoSkipIntroDescription": "Bir neçə saniyədən sonra giriş işarələrini avtomatik ötür", "autoSkipCredits": "Titrləri avtomatik ötür", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "Daxili DV7-ni məcburi et", "dvConversionDv81Description": "Dolby Vision profile 8.1-ə çevrilməni məcburi et", "dvConversionHevcStripDescription": "Dolby Vision təbəqələrini sil və sadə HEVC kimi təqdim et", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "Açılışda profil soruş", "requireProfileSelectionOnOpenDescription": "Tətbiq hər dəfə açıldıqda profil seçimini göstər", "forceTvMode": "TV rejimini məcburi et", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "Sarğı və vaxt tənzimləməsi", "behavior": "Davranış", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "Pleyer dəyişikliklərini yadda saxla", "rememberPlayerChangesDescription": "Oxutma zamanı edilən dəyişikliklərin harada saxlanacağı və yenidən tətbiq ediləcəyi", diff --git a/lib/i18n/bg.i18n.json b/lib/i18n/bg.i18n.json index b4e5f34a6..885d706ed 100644 --- a/lib/i18n/bg.i18n.json +++ b/lib/i18n/bg.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "Автоматично прескачане на интро", "autoSkipIntroDescription": "Автоматично прескачай интро маркери след няколко секунди", "autoSkipCredits": "Автоматично прескачане на финални надписи", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "Принуждава директно възпроизвеждане на DV7 и изключва повторния опит за преобразуване", "dvConversionDv81Description": "Принуждава директно преобразуване на RPU към Dolby Vision Profile 8.1", "dvConversionHevcStripDescription": "Премахва слоевете Dolby Vision RPU/EL и подава обикновен HEVC поток", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "Питай за профил при отваряне на приложението", "requireProfileSelectionOnOpenDescription": "Показвай избор на профил всеки път при отваряне на приложението", "forceTvMode": "Принуди TV режим", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "Търсене и време", "behavior": "Поведение", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "Запомняне на промените в плейъра", "rememberPlayerChangesDescription": "Къде се записва и откъде се прилага отново промяна, направена по време на възпроизвеждане", diff --git a/lib/i18n/da.i18n.json b/lib/i18n/da.i18n.json index e13a32351..c0cb7677d 100644 --- a/lib/i18n/da.i18n.json +++ b/lib/i18n/da.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "Spring intro over automatisk", "autoSkipIntroDescription": "Spring automatisk intromarkører over efter få sekunder", "autoSkipCredits": "Spring rulletekster over automatisk", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "Gennemtving indbygget DV7-understøttelse, og undlad at forsøge DV-konvertering igen", "dvConversionDv81Description": "Tving inline RPU-konvertering til Dolby Vision profil 8.1", "dvConversionHevcStripDescription": "Fjern Dolby Vision RPU/EL-lag og brug almindelig HEVC", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "Spørg om profil ved åbning", "requireProfileSelectionOnOpenDescription": "Vis profilvalg hver gang appen åbnes", "forceTvMode": "Gennemtving TV-tilstand", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "Søgning og timing", "behavior": "Adfærd", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "Husk afspillerændringer", "rememberPlayerChangesDescription": "Hvor en ændring under afspilning gemmes og anvendes igen", diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index 60a0f6253..ec89bcda7 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "Intro automatisch überspringen", "autoSkipIntroDescription": "Intro-Marker nach wenigen Sekunden automatisch überspringen", "autoSkipCredits": "Abspann automatisch überspringen", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "Natives DV7 erzwingen und einen erneuten DV-Konvertierungsversuch unterdrücken", "dvConversionDv81Description": "Inline-RPU-Konvertierung in Dolby-Vision-Profil 8.1 erzwingen", "dvConversionHevcStripDescription": "Dolby-Vision-RPU/EL-Schichten entfernen und reines HEVC ausgeben", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "Profil beim Öffnen abfragen", "requireProfileSelectionOnOpenDescription": "Profilauswahl bei jedem Öffnen der App anzeigen", "forceTvMode": "TV-Modus erzwingen", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "Spulen & Timing", "behavior": "Verhalten", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "Playeränderungen merken", "rememberPlayerChangesDescription": "Wo eine während der Wiedergabe vorgenommene Änderung gespeichert und erneut angewendet wird", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 5ce23175b..44737a8df 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "Auto-Play & Skip", "autoPlayNextEpisode": "Auto-Play Next Episode", "autoPlayNextEpisodeDescription": "Start the next episode automatically when one ends", + "playNextCountdown": "Play Next Countdown", + "playNextCountdownImmediate": "Play immediately", "autoSkipIntro": "Auto Skip Intro", "autoSkipIntroDescription": "Automatically skip intro markers after a few seconds", "autoSkipCredits": "Auto Skip Credits", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "Force native DV7 and suppress DV conversion retry", "dvConversionDv81Description": "Force inline RPU conversion to Dolby Vision profile 8.1", "dvConversionHevcStripDescription": "Strip Dolby Vision RPU/EL layers and present plain HEVC", + "deinterlace": "Deinterlacing", + "deinterlaceDescription": "Remove combing artifacts from interlaced video (mpv player only)", "requireProfileSelectionOnOpen": "Ask for profile on app open", "requireProfileSelectionOnOpenDescription": "Show profile selection every time the app is opened", "forceTvMode": "Force TV mode", @@ -374,6 +378,13 @@ "subtitles": "Subtitles", "seekAndTiming": "Seek & Timing", "behavior": "Behavior", + "gestures": "Gestures", + "gestureBrightnessSwipe": "Brightness Swipe", + "gestureBrightnessSwipeDescription": "Swipe up or down on the left edge to adjust brightness", + "gestureVolumeSwipe": "Volume Swipe", + "gestureVolumeSwipeDescription": "Swipe up or down on the right edge to adjust volume", + "gesturePinchToZoom": "Pinch to Zoom", + "gesturePinchToZoomDescription": "Pinch on the video to zoom in or out", "controls": "Controls", "rememberPlayerChanges": "Remember Player Changes", "rememberPlayerChangesDescription": "Where a change made during playback is saved and reapplied from", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index 11297dcc2..bb62f5d9f 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "Saltar introducción automáticamente", "autoSkipIntroDescription": "Saltar automáticamente los marcadores de introducción después de unos segundos", "autoSkipCredits": "Saltar créditos automáticamente", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "Forzar DV7 nativo y suprimir el reintento de conversión DV", "dvConversionDv81Description": "Forzar la conversión de RPU en línea al perfil 8.1 de Dolby Vision", "dvConversionHevcStripDescription": "Eliminar las capas RPU/EL de Dolby Vision y presentar HEVC convencional", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "Pedir perfil al abrir la aplicación", "requireProfileSelectionOnOpenDescription": "Mostrar selección de perfil cada vez que se abre la aplicación", "forceTvMode": "Forzar modo TV", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "Desplazamiento y tiempos", "behavior": "Comportamiento", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "Recordar cambios del reproductor", "rememberPlayerChangesDescription": "Dónde se guarda y se vuelve a aplicar un cambio realizado durante la reproducción", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index 93eac694b..d7391c967 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "Passer automatiquement l’introduction", "autoSkipIntroDescription": "Passer automatiquement les marqueurs d’introduction après quelques secondes", "autoSkipCredits": "Passer automatiquement le générique", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "Forcer le DV7 natif et bloquer la nouvelle tentative de conversion DV", "dvConversionDv81Description": "Forcer la conversion RPU intégrée vers le profil 8.1 de Dolby Vision", "dvConversionHevcStripDescription": "Supprimer les couches RPU/EL Dolby Vision et présenter du HEVC simple", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "Demander le profil à l'ouverture", "requireProfileSelectionOnOpenDescription": "Afficher la sélection de profil à chaque ouverture de l'application", "forceTvMode": "Forcer le mode TV", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "Déplacement et minutage", "behavior": "Comportement", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "Mémoriser les modifications du lecteur", "rememberPlayerChangesDescription": "Où une modification effectuée pendant la lecture est enregistrée et réappliquée", diff --git a/lib/i18n/hu.i18n.json b/lib/i18n/hu.i18n.json index 076abee0a..267cd3787 100644 --- a/lib/i18n/hu.i18n.json +++ b/lib/i18n/hu.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "Intró automatikus átugrása", "autoSkipIntroDescription": "Az intrójelölők automatikus átugrása néhány másodperc után", "autoSkipCredits": "Stáblista automatikus átugrása", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "A natív DV7 kényszerítése és a DV-átalakítási újrapróbálkozás letiltása", "dvConversionDv81Description": "A közvetlen RPU-átalakítás kényszerítése Dolby Vision Profile 8.1-re", "dvConversionHevcStripDescription": "A Dolby Vision RPU/EL-rétegek eltávolítása és egyszerű HEVC-ként való megjelenítés", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "Profil kérése az alkalmazás megnyitásakor", "requireProfileSelectionOnOpenDescription": "Profilválasztó megjelenítése minden alkalommal, amikor az alkalmazást megnyitod", "forceTvMode": "TV-mód kényszerítése", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "Tekerés és időzítés", "behavior": "Viselkedés", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "Lejátszó módosításainak megjegyzése", "rememberPlayerChangesDescription": "A lejátszás közben végzett módosítások mentési és újbóli alkalmazási helye", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index fd06f346d..a9ed37f5c 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "Salta automaticamente la sigla iniziale", "autoSkipIntroDescription": "Salta automaticamente i marcatori della sigla iniziale dopo alcuni secondi", "autoSkipCredits": "Salta automaticamente i titoli di coda", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "Forza il DV7 nativo e impedisce nuovi tentativi di conversione DV", "dvConversionDv81Description": "Forza la conversione RPU diretta al profilo Dolby Vision 8.1", "dvConversionHevcStripDescription": "Rimuove i livelli RPU/EL di Dolby Vision e riproduce il video come semplice HEVC", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "Chiedi di scegliere il profilo all'apertura", "requireProfileSelectionOnOpenDescription": "Mostra la selezione del profilo ogni volta che l'app viene aperta", "forceTvMode": "Forza modalità TV", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "Avanzamento e tempi", "behavior": "Comportamento", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "Ricorda le modifiche del lettore", "rememberPlayerChangesDescription": "Dove viene salvata e riapplicata una modifica effettuata durante la riproduzione", diff --git a/lib/i18n/ja.i18n.json b/lib/i18n/ja.i18n.json index 6de4ba603..849afd5bb 100644 --- a/lib/i18n/ja.i18n.json +++ b/lib/i18n/ja.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "イントロを自動スキップ", "autoSkipIntroDescription": "数秒後にイントロマーカーを自動的にスキップ", "autoSkipCredits": "クレジットを自動スキップ", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "ネイティブ DV7 を強制し、DV 変換の再試行を抑制します", "dvConversionDv81Description": "Dolby Vision プロファイル 8.1 へのインライン RPU 変換を強制します", "dvConversionHevcStripDescription": "Dolby Vision の RPU/EL レイヤーを削除し、通常の HEVC として扱います", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "アプリ起動時にプロフィールを確認", "requireProfileSelectionOnOpenDescription": "アプリを開くたびにプロフィール選択を表示", "forceTvMode": "TVモードを強制", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "シークとタイミング", "behavior": "動作", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "プレーヤーの変更を記憶", "rememberPlayerChangesDescription": "再生中に行った変更を保存し、再適用する場所", diff --git a/lib/i18n/kk.i18n.json b/lib/i18n/kk.i18n.json index af6c0c6e7..1dd306ef4 100644 --- a/lib/i18n/kk.i18n.json +++ b/lib/i18n/kk.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "Киріс бөлімді (Intro) автоматты өткізу", "autoSkipIntroDescription": "Бірнеше секундтан кейін киріс белгілерін автоматты өткізу", "autoSkipCredits": "Титрлерді автоматты өткізу", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "Ішкі DV7 режимін мәжбүрлеу", "dvConversionDv81Description": "Dolby Vision profile 8.1 форматына түрлендіру", "dvConversionHevcStripDescription": "Dolby Vision қабаттарын алып тастап, HEVC ретінде көрсету", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "Ашқанда профильді сұрау", "requireProfileSelectionOnOpenDescription": "Қолданба ашылған сайын профильді таңдауды көрсету", "forceTvMode": "TV режимін мәжбүрлеу", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "Айналдыру және уақытты реттеу", "behavior": "Мінез-құлық", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "Ойнатқыш өзгерістерін есте сақтау", "rememberPlayerChangesDescription": "Ойнату кезінде жасалған өзгеріс сақталатын және қайта қолданылатын орын", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 7a4572fb8..3c712dcea 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "자동으로 오프닝 건너뛰기", "autoSkipIntroDescription": "몇 초 후 오프닝을 자동으로 건너뛰기", "autoSkipCredits": "자동으로 엔딩 건너뛰기", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "네이티브 DV7을 강제하고 DV 변환 재시도를 억제합니다", "dvConversionDv81Description": "Dolby Vision 프로필 8.1로 인라인 RPU 변환을 강제합니다", "dvConversionHevcStripDescription": "Dolby Vision RPU/EL 레이어를 제거하고 일반 HEVC로 제공합니다", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "앱 실행 시 프로필 선택", "requireProfileSelectionOnOpenDescription": "앱을 열 때마다 프로필 선택 화면을 표시합니다", "forceTvMode": "TV 모드 강제 사용", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "탐색 및 타이밍", "behavior": "동작", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "플레이어 변경 사항 기억", "rememberPlayerChangesDescription": "재생 중 변경한 사항을 저장하고 다시 적용할 위치", diff --git a/lib/i18n/nb.i18n.json b/lib/i18n/nb.i18n.json index 6c95b1736..a3aa58a0f 100644 --- a/lib/i18n/nb.i18n.json +++ b/lib/i18n/nb.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "Hopp over intro automatisk", "autoSkipIntroDescription": "Hopp automatisk over intromarkører etter noen sekunder", "autoSkipCredits": "Hopp over rulletekst automatisk", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "Tving opprinnelig DV7-avspilling og ikke prøv DV-konvertering på nytt", "dvConversionDv81Description": "Tving direkte RPU-konvertering til Dolby Vision-profil 8.1", "dvConversionHevcStripDescription": "Fjern Dolby Vision RPU/EL-lag og lever som vanlig HEVC", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "Spør om profil ved appåpning", "requireProfileSelectionOnOpenDescription": "Vis profilvalg hver gang appen åpnes", "forceTvMode": "Tving TV-modus", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "Spoling og tidsinnstillinger", "behavior": "Oppførsel", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "Husk endringer i spilleren", "rememberPlayerChangesDescription": "Hvor en endring under avspilling lagres og brukes på nytt", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index dfadf62b5..8537f2884 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "Intro automatisch overslaan", "autoSkipIntroDescription": "Intromarkeringen na enkele seconden automatisch overslaan", "autoSkipCredits": "Aftiteling automatisch overslaan", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "Dwing native DV7 af en voorkom een nieuwe poging met DV-conversie", "dvConversionDv81Description": "Dwing directe RPU-conversie naar Dolby Vision-profiel 8.1 af", "dvConversionHevcStripDescription": "Verwijder Dolby Vision RPU/EL-lagen en bied gewone HEVC aan", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "Vraag om profiel bij openen", "requireProfileSelectionOnOpenDescription": "Toon profielselectie telkens wanneer de app wordt geopend", "forceTvMode": "Tv-modus afdwingen", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "Spoelen en timing", "behavior": "Gedrag", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "Spelerwijzigingen onthouden", "rememberPlayerChangesDescription": "Waar een wijziging tijdens het afspelen wordt opgeslagen en opnieuw toegepast", diff --git a/lib/i18n/pl.i18n.json b/lib/i18n/pl.i18n.json index a041fa9de..05fc321bb 100644 --- a/lib/i18n/pl.i18n.json +++ b/lib/i18n/pl.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "Automatyczne pomijanie intro", "autoSkipIntroDescription": "Automatycznie pomijaj znaczniki intro po kilku sekundach", "autoSkipCredits": "Automatyczne pomijanie napisów końcowych", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "Wymuś natywne DV7 i wyłącz ponowną próbę konwersji DV", "dvConversionDv81Description": "Wymuś wbudowaną konwersję RPU do profilu Dolby Vision 8.1", "dvConversionHevcStripDescription": "Usuń warstwy Dolby Vision RPU/EL i przedstaw zwykłe HEVC", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "Pytaj o profil przy otwarciu aplikacji", "requireProfileSelectionOnOpenDescription": "Pokaż wybór profilu za każdym razem, gdy aplikacja jest otwierana", "forceTvMode": "Wymuś tryb TV", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "Przewijanie i czas", "behavior": "Zachowanie", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "Zapamiętuj zmiany odtwarzacza", "rememberPlayerChangesDescription": "Miejsce zapisywania i ponownego stosowania zmian dokonanych podczas odtwarzania", diff --git a/lib/i18n/pt.i18n.json b/lib/i18n/pt.i18n.json index 48826ab06..be9cbd061 100644 --- a/lib/i18n/pt.i18n.json +++ b/lib/i18n/pt.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "Pular introdução automaticamente", "autoSkipIntroDescription": "Pular marcadores de introdução automaticamente após alguns segundos", "autoSkipCredits": "Pular créditos automaticamente", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "Forçar DV7 nativo e impedir uma nova tentativa de conversão de DV", "dvConversionDv81Description": "Forçar a conversão RPU integrada para Dolby Vision perfil 8.1", "dvConversionHevcStripDescription": "Remover as camadas RPU/EL do Dolby Vision e apresentar HEVC sem Dolby Vision", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "Pedir perfil ao abrir o app", "requireProfileSelectionOnOpenDescription": "Mostrar a seleção de perfil sempre que o app for aberto", "forceTvMode": "Forçar modo TV", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "Busca e tempo", "behavior": "Comportamento", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "Lembrar alterações do reprodutor", "rememberPlayerChangesDescription": "Onde uma alteração feita durante a reprodução é salva e reaplicada", diff --git a/lib/i18n/ru.i18n.json b/lib/i18n/ru.i18n.json index b6790ca3c..e379c8c2d 100644 --- a/lib/i18n/ru.i18n.json +++ b/lib/i18n/ru.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "Автопропуск вступления", "autoSkipIntroDescription": "Автоматически пропускать маркеры вступления через несколько секунд", "autoSkipCredits": "Автопропуск титров", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "Принудительно использовать нативный DV7 и не повторять DV-конвертацию", "dvConversionDv81Description": "Принудительно выполнять inline-конвертацию RPU в Dolby Vision профиль 8.1", "dvConversionHevcStripDescription": "Удалять слои Dolby Vision RPU/EL и передавать обычный HEVC", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "Запрашивать профиль при запуске", "requireProfileSelectionOnOpenDescription": "Показывать выбор профиля при каждом открытии приложения", "forceTvMode": "Принудительный режим ТВ", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "Перемотка и время", "behavior": "Поведение", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "Запоминать изменения плеера", "rememberPlayerChangesDescription": "Где сохраняется и откуда повторно применяется изменение, сделанное во время воспроизведения", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 02e4f424d..ac6be5e24 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 22 -/// Strings: 41573 (1889 per locale) +/// Strings: 41584 (1890 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index 2a113c824..0d25ac6ff 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -837,6 +837,12 @@ class Translations$settings$en { /// en: 'Start the next episode automatically when one ends' String get autoPlayNextEpisodeDescription => 'Start the next episode automatically when one ends'; + /// en: 'Play Next Countdown' + String get playNextCountdown => 'Play Next Countdown'; + + /// en: 'Play immediately' + String get playNextCountdownImmediate => 'Play immediately'; + /// en: 'Auto Skip Intro' String get autoSkipIntro => 'Auto Skip Intro'; @@ -1059,6 +1065,12 @@ class Translations$settings$en { /// en: 'Strip Dolby Vision RPU/EL layers and present plain HEVC' String get dvConversionHevcStripDescription => 'Strip Dolby Vision RPU/EL layers and present plain HEVC'; + /// en: 'Deinterlacing' + String get deinterlace => 'Deinterlacing'; + + /// en: 'Remove combing artifacts from interlaced video (mpv player only)' + String get deinterlaceDescription => 'Remove combing artifacts from interlaced video (mpv player only)'; + /// en: 'Ask for profile on app open' String get requireProfileSelectionOnOpen => 'Ask for profile on app open'; @@ -1161,6 +1173,27 @@ class Translations$settings$en { /// en: 'Behavior' String get behavior => 'Behavior'; + /// en: 'Gestures' + String get gestures => 'Gestures'; + + /// en: 'Brightness Swipe' + String get gestureBrightnessSwipe => 'Brightness Swipe'; + + /// en: 'Swipe up or down on the left edge to adjust brightness' + String get gestureBrightnessSwipeDescription => 'Swipe up or down on the left edge to adjust brightness'; + + /// en: 'Volume Swipe' + String get gestureVolumeSwipe => 'Volume Swipe'; + + /// en: 'Swipe up or down on the right edge to adjust volume' + String get gestureVolumeSwipeDescription => 'Swipe up or down on the right edge to adjust volume'; + + /// en: 'Pinch to Zoom' + String get gesturePinchToZoom => 'Pinch to Zoom'; + + /// en: 'Pinch on the video to zoom in or out' + String get gesturePinchToZoomDescription => 'Pinch on the video to zoom in or out'; + /// en: 'Controls' String get controls => 'Controls'; @@ -7103,6 +7136,8 @@ extension on Translations { 'settings.autoPlayAndSkip' => 'Auto-Play & Skip', 'settings.autoPlayNextEpisode' => 'Auto-Play Next Episode', 'settings.autoPlayNextEpisodeDescription' => 'Start the next episode automatically when one ends', + 'settings.playNextCountdown' => 'Play Next Countdown', + 'settings.playNextCountdownImmediate' => 'Play immediately', 'settings.autoSkipIntro' => 'Auto Skip Intro', 'settings.autoSkipIntroDescription' => 'Automatically skip intro markers after a few seconds', 'settings.autoSkipCredits' => 'Auto Skip Credits', @@ -7177,6 +7212,8 @@ extension on Translations { 'settings.dvConversionNativeDescription' => 'Force native DV7 and suppress DV conversion retry', 'settings.dvConversionDv81Description' => 'Force inline RPU conversion to Dolby Vision profile 8.1', 'settings.dvConversionHevcStripDescription' => 'Strip Dolby Vision RPU/EL layers and present plain HEVC', + 'settings.deinterlace' => 'Deinterlacing', + 'settings.deinterlaceDescription' => 'Remove combing artifacts from interlaced video (mpv player only)', 'settings.requireProfileSelectionOnOpen' => 'Ask for profile on app open', 'settings.requireProfileSelectionOnOpenDescription' => 'Show profile selection every time the app is opened', 'settings.forceTvMode' => 'Force TV mode', @@ -7211,6 +7248,13 @@ extension on Translations { 'settings.subtitles' => 'Subtitles', 'settings.seekAndTiming' => 'Seek & Timing', 'settings.behavior' => 'Behavior', + 'settings.gestures' => 'Gestures', + 'settings.gestureBrightnessSwipe' => 'Brightness Swipe', + 'settings.gestureBrightnessSwipeDescription' => 'Swipe up or down on the left edge to adjust brightness', + 'settings.gestureVolumeSwipe' => 'Volume Swipe', + 'settings.gestureVolumeSwipeDescription' => 'Swipe up or down on the right edge to adjust volume', + 'settings.gesturePinchToZoom' => 'Pinch to Zoom', + 'settings.gesturePinchToZoomDescription' => 'Pinch on the video to zoom in or out', 'settings.controls' => 'Controls', 'settings.rememberPlayerChanges' => 'Remember Player Changes', 'settings.rememberPlayerChangesDescription' => 'Where a change made during playback is saved and reapplied from', @@ -7352,6 +7396,8 @@ extension on Translations { 'fileInfo.sourceKind' => 'Source Kind', 'fileInfo.optimizedVersion' => 'Optimized Version', 'fileInfo.optimizationTarget' => 'Optimization Target', + _ => null, + } ?? switch (path) { 'fileInfo.deletedAt' => 'Deleted', 'fileInfo.remoteSource' => 'Remote Source', 'fileInfo.infiniteStream' => 'Infinite Stream', @@ -7363,8 +7409,6 @@ extension on Translations { 'fileInfo.fileId' => 'File ID', 'fileInfo.defaultAudioTrack' => 'Default Audio Track', 'fileInfo.defaultSubtitleTrack' => 'Default Subtitle Track', - _ => null, - } ?? switch (path) { 'fileInfo.subtitlesOff' => 'Off', 'fileInfo.flagDefault' => 'Default', 'fileInfo.flagForced' => 'Forced', @@ -7866,6 +7910,8 @@ extension on Translations { 'libraries.filterCategories.unplayed' => 'Unplayed', 'libraries.filterCategories.favorites' => 'Favorites', 'libraries.sortLabels.title' => 'Title', + _ => null, + } ?? switch (path) { 'libraries.sortLabels.dateAdded' => 'Date Added', 'libraries.sortLabels.releaseDate' => 'Release Date', 'libraries.sortLabels.rating' => 'Rating', @@ -7877,8 +7923,6 @@ extension on Translations { 'libraries.sortLabels.productionYear' => 'Production Year', 'libraries.sortLabels.runtime' => 'Runtime', 'libraries.sortLabels.officialRating' => 'Official Rating', - _ => null, - } ?? switch (path) { 'libraries.sortLabels.premiereDate' => 'Premiere Date', 'libraries.sortLabels.startDate' => 'Start Date', 'libraries.sortLabels.airTime' => 'Air Time', @@ -8380,6 +8424,8 @@ extension on Translations { 'downloads.syncRuleAvailable' => 'Available', 'downloads.syncRuleOffline' => 'Offline', 'downloads.syncRuleSignInRequired' => 'Sign in required', + _ => null, + } ?? switch (path) { 'downloads.syncRuleNotAvailableForProfile' => 'Not available for current profile', 'downloads.syncRuleUnknownServer' => 'Unknown server', 'downloads.syncRuleListCreated' => 'Sync rule created', @@ -8391,8 +8437,6 @@ extension on Translations { 'downloads.backgroundWarning.sheetIntro' => 'Android is preventing Plezy from downloading reliably in the background.', 'downloads.backgroundWarning.sheetIntroDegraded' => 'Your device is limiting when Plezy can download in the background.', 'downloads.backgroundWarning.reasonBackgroundRestricted' => 'Plezy\'s background usage is restricted. Set its battery or background usage to "Unrestricted".', - _ => null, - } ?? switch (path) { 'downloads.backgroundWarning.reasonStandbyRestricted' => 'Android has put Plezy in a restricted standby state. Set its battery usage to "Unrestricted".', 'downloads.backgroundWarning.reasonDownloadChannelBlocked' => 'Download notifications are turned off, so progress and controls may be unavailable.', 'downloads.backgroundWarning.reasonNotificationsDisabled' => 'Notifications are turned off. On Android 13 or newer, they are required for long background downloads.', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index d4657bf2e..21e23a023 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "Hoppa över intro automatiskt", "autoSkipIntroDescription": "Hoppa automatiskt över intromarkörer efter några sekunder", "autoSkipCredits": "Hoppa över eftertexter automatiskt", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "Tvinga inbyggd DV7 och förhindra nya försök med DV-konvertering", "dvConversionDv81Description": "Tvinga direkt RPU-konvertering till Dolby Vision-profil 8.1", "dvConversionHevcStripDescription": "Ta bort Dolby Visions RPU-/EL-lager och använd vanlig HEVC", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "Fråga efter profil vid appstart", "requireProfileSelectionOnOpenDescription": "Visa profilval varje gång appen öppnas", "forceTvMode": "Tvinga TV-läge", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "Spolning och tidsinställningar", "behavior": "Beteende", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "Kom ihåg spelarändringar", "rememberPlayerChangesDescription": "Var en ändring under uppspelning sparas och tillämpas igen", diff --git a/lib/i18n/tr.i18n.json b/lib/i18n/tr.i18n.json index 003559b85..306b74d1c 100644 --- a/lib/i18n/tr.i18n.json +++ b/lib/i18n/tr.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "Jeneriği Otomatik Atla", "autoSkipIntroDescription": "Kaç saniye sonra jenerik işaretçilerini otomatik atla", "autoSkipCredits": "Emeği Geçenleri Otomatik Atla", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "Yerel DV7'yi zorla ve DV dönüştürme yeniden denemesini engelle", "dvConversionDv81Description": "Dolby Vision profile 8.1'e satır içi RPU dönüştürmeyi zorla", "dvConversionHevcStripDescription": "Dolby Vision RPU/EL katmanlarını soy ve düz HEVC olarak sun", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "Uygulama açılışında profil sor", "requireProfileSelectionOnOpenDescription": "Uygulama her açıldığında profil seçimini göster", "forceTvMode": "TV modunu zorla", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "Sarılma ve Zamanlama", "behavior": "Davranış", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "Oynatıcı değişikliklerini hatırla", "rememberPlayerChangesDescription": "Oynatma sırasında yapılan değişikliklerin kaydedilip yeniden uygulanacağı yer", diff --git a/lib/i18n/uz.i18n.json b/lib/i18n/uz.i18n.json index 1403abf54..636ef1289 100644 --- a/lib/i18n/uz.i18n.json +++ b/lib/i18n/uz.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "Kirish qismini (Intro) avtomatik oʻtkazish", "autoSkipIntroDescription": "Bir necha soniyadan soʻng kirish qismlarini avtomatik oʻtkazish", "autoSkipCredits": "Titrlarni avtomatik oʻtkazish", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "Ichki DV7 rejimini majburlash", "dvConversionDv81Description": "Dolby Vision profile 8.1 formatiga oʻtkazish", "dvConversionHevcStripDescription": "Dolby Vision qatlamlarini olib tashlash va HEVC sifatida koʻrsatish", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "Ochilganda profilni soʻrash", "requireProfileSelectionOnOpenDescription": "Ilova ochilgan har safar profilni tanlashni koʻrsatish", "forceTvMode": "TV rejimini majburlash", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "Oʻtkazish va vaqt sozlamalari", "behavior": "Xatti-harakat", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "Pleyer oʻzgarishlarini eslab qolish", "rememberPlayerChangesDescription": "Ijro vaqtida qilingan oʻzgarish qayerda saqlanishi va qayta qoʻllanishi", diff --git a/lib/i18n/zh-Hant.i18n.json b/lib/i18n/zh-Hant.i18n.json index 7137e4f96..fddcae40e 100644 --- a/lib/i18n/zh-Hant.i18n.json +++ b/lib/i18n/zh-Hant.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "自動跳過片頭", "autoSkipIntroDescription": "幾秒鐘後自動跳過片頭標記", "autoSkipCredits": "自動跳過片尾", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "強制使用原生 DV7 並停用 DV 轉換重試", "dvConversionDv81Description": "強制將內嵌的 RPU 轉換為 Dolby Vision Profile 8.1", "dvConversionHevcStripDescription": "移除 Dolby Vision RPU/EL 層,並以一般 HEVC 呈現", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "開啟應用程式時要求選擇使用者", "requireProfileSelectionOnOpenDescription": "每次開啟應用程式時顯示使用者設定檔選擇畫面", "forceTvMode": "強制 TV 模式", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "跳轉與計時", "behavior": "行為", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "記住播放器變更", "rememberPlayerChangesDescription": "播放期間所做的變更要儲存並從何處重新套用", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index b50ca0659..f1b6c0d3a 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -266,6 +266,8 @@ "autoPlayAndSkip": "", "autoPlayNextEpisode": "", "autoPlayNextEpisodeDescription": "", + "playNextCountdown": "", + "playNextCountdownImmediate": "", "autoSkipIntro": "自动跳过片头", "autoSkipIntroDescription": "几秒钟后自动跳过片头标记", "autoSkipCredits": "自动跳过片尾", @@ -340,6 +342,8 @@ "dvConversionNativeDescription": "强制原生 DV7 并禁止重试 DV 转换", "dvConversionDv81Description": "强制内联 RPU 转换为 Dolby Vision Profile 8.1", "dvConversionHevcStripDescription": "移除 Dolby Vision RPU/EL 层并呈现普通 HEVC", + "deinterlace": "", + "deinterlaceDescription": "", "requireProfileSelectionOnOpen": "打开应用时选择用户资料", "requireProfileSelectionOnOpenDescription": "每次打开应用时都显示用户资料选择界面", "forceTvMode": "强制 TV 模式", @@ -374,6 +378,13 @@ "subtitles": "", "seekAndTiming": "跳转与计时", "behavior": "行为", + "gestures": "", + "gestureBrightnessSwipe": "", + "gestureBrightnessSwipeDescription": "", + "gestureVolumeSwipe": "", + "gestureVolumeSwipeDescription": "", + "gesturePinchToZoom": "", + "gesturePinchToZoomDescription": "", "controls": "", "rememberPlayerChanges": "记住播放器更改", "rememberPlayerChangesDescription": "播放期间所做的更改保存并重新应用的位置", diff --git a/lib/screens/settings/general_settings_screen.dart b/lib/screens/settings/general_settings_screen.dart index 36cb7f8aa..e023057e7 100644 --- a/lib/screens/settings/general_settings_screen.dart +++ b/lib/screens/settings/general_settings_screen.dart @@ -32,15 +32,7 @@ class GeneralSettingsScreen extends StatelessWidget { return SettingsPage( title: Text(t.settings.general), children: [ - SettingsGroup( - title: t.settings.languageAndRegion, - children: [ - _languageSelector(context), - // TODO: Metadata language for Plex discover/metadata queries (#1668) - // goes here once the client threads a language override through - // the catalog/watchlist requests. - ], - ), + SettingsGroup(title: t.settings.languageAndRegion, children: [_languageSelector(context)]), SettingsGroup( title: t.settings.startup, @@ -77,9 +69,6 @@ class GeneralSettingsScreen extends StatelessWidget { title: t.settings.startInFullscreen, subtitle: t.settings.startInFullscreenDescription, ), - // TODO: Always-on-top selector (Off / Always / While playing) - // (#1386, #931) goes here; needs a persisted pref replacing the - // session-only window flag. ], ), const SizedBox(height: 24), diff --git a/lib/screens/settings/playback_settings_screen.dart b/lib/screens/settings/playback_settings_screen.dart index 486438068..eaba6bdf0 100644 --- a/lib/screens/settings/playback_settings_screen.dart +++ b/lib/screens/settings/playback_settings_screen.dart @@ -66,9 +66,6 @@ class _PlaybackSettingsScreenState extends State { (svc.read(SettingsService.matchRefreshRate) || svc.read(SettingsService.matchDynamicRange))) || (Platform.isAndroid && (svc.read(SettingsService.matchContentFrameRate) || svc.read(SettingsService.matchContentResolution))); - // Every Video & Display tile is platform-gated; skip the group where - // none can appear so it does not render as an empty card. - final hasVideoDisplayGroup = Platform.isAndroid || Platform.isWindows || showDisplaySwitchDelay; return SettingsPage( title: Text(t.settings.videoPlayback), @@ -86,24 +83,22 @@ class _PlaybackSettingsScreenState extends State { ], ), - if (hasVideoDisplayGroup) - SettingsGroup( - title: t.settings.videoAndDisplay, - children: [ - if (Platform.isAndroid) _matchContentFrameRateTile(), - if (Platform.isAndroid && PlatformDetector.isTV()) _matchContentResolutionTile(), - if (Platform.isWindows) _matchRefreshRateTile(), - if (Platform.isWindows) _matchDynamicRangeTile(), - if (showDisplaySwitchDelay) _displaySwitchDelayTile(), - if (Platform.isAndroid) _dvConversionModeTile(), - // TODO: Deinterlace toggle (#2149) goes here, mpv-only — - // record ExoPlayer as intentionally unsupported and drop the - // hasVideoDisplayGroup gate once the tile exists on every - // mpv platform. - // TODO: "Extend video into display cutout" toggle (#1769) - // goes here, Android-only. - ], - ), + SettingsGroup( + title: t.settings.videoAndDisplay, + children: [ + if (Platform.isAndroid) _matchContentFrameRateTile(), + if (Platform.isAndroid && PlatformDetector.isTV()) _matchContentResolutionTile(), + if (Platform.isWindows) _matchRefreshRateTile(), + if (Platform.isWindows) _matchDynamicRangeTile(), + if (showDisplaySwitchDelay) _displaySwitchDelayTile(), + if (Platform.isAndroid) _dvConversionModeTile(), + // mpv-only (#2149): ExoPlayer has no filter chain, so the + // tile disappears while the ExoPlayer backend is active. + if (!exoActive) _deinterlaceTile(), + // TODO: "Extend video into display cutout" toggle (#1769) + // goes here, Android-only. + ], + ), SettingsGroup( title: t.settings.audio, @@ -146,9 +141,7 @@ class _PlaybackSettingsScreenState extends State { _seekAndTimingGroup(), _autoPlayAndSkipGroup(), _behaviorGroup(context, isMobile), - // TODO: Gestures group (#1810, #1046) goes here, mobile-only: - // per-gesture toggles for volume, brightness, zoom, and the - // hold-to-speed gesture during playback. + if (isMobile) _gesturesGroup(), _rememberPlayerChangesGroup(), const SizedBox(height: 24), ], @@ -314,8 +307,17 @@ class _PlaybackSettingsScreenState extends State { title: t.settings.autoPlayNextEpisode, subtitle: t.settings.autoPlayNextEpisodeDescription, ), - // TODO: "Play next countdown" duration tile (#1827) goes here, beside - // autoPlayNextEpisode. + SettingNumberTile( + pref: SettingsService.playNextCountdown, + icon: Symbols.timer_rounded, + title: t.settings.playNextCountdown, + subtitleBuilder: (v) => + v == 0 ? t.settings.playNextCountdownImmediate : t.settings.secondsUnit(seconds: v.toString()), + labelText: t.settings.secondsLabel, + suffixText: t.settings.secondsShort, + min: 0, + max: 30, + ), // TODO: Replace the two auto-skip switches below with per-marker skip // modes — Off / Show button / Auto (#2138); migrate true→auto, // false→button in SettingsService. @@ -364,6 +366,32 @@ class _PlaybackSettingsScreenState extends State { ], ); + /// Optional touch gestures on the player surface (#1810); the group only + /// renders on mobile, matching where the gestures exist. + Widget _gesturesGroup() => SettingsGroup( + title: t.settings.gestures, + children: [ + SettingSwitchTile( + pref: SettingsService.gestureBrightnessSwipe, + icon: Symbols.brightness_6_rounded, + title: t.settings.gestureBrightnessSwipe, + subtitle: t.settings.gestureBrightnessSwipeDescription, + ), + SettingSwitchTile( + pref: SettingsService.gestureVolumeSwipe, + icon: Symbols.volume_up_rounded, + title: t.settings.gestureVolumeSwipe, + subtitle: t.settings.gestureVolumeSwipeDescription, + ), + SettingSwitchTile( + pref: SettingsService.gesturePinchToZoom, + icon: Symbols.pinch_rounded, + title: t.settings.gesturePinchToZoom, + subtitle: t.settings.gesturePinchToZoomDescription, + ), + ], + ); + Widget _playerBackendSelector() => SettingSegmentedTile( pref: SettingsService.useExoPlayer, icon: Symbols.play_circle_rounded, @@ -436,6 +464,13 @@ class _PlaybackSettingsScreenState extends State { subtitle: t.settings.matchDynamicRangeDescription, ); + Widget _deinterlaceTile() => SettingSwitchTile( + pref: SettingsService.deinterlace, + icon: Symbols.deblur_rounded, + title: t.settings.deinterlace, + subtitle: t.settings.deinterlaceDescription, + ); + Widget _audioPassthroughTile() => SettingSwitchTile( pref: SettingsService.audioPassthrough, icon: Symbols.surround_sound_rounded, diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index 4a43ace69..0fd438672 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -435,8 +435,6 @@ class _SettingsScreenState extends State with FocusableTab, Moun subtitle: t.settings.autoRemoveWatchedDownloadsDescription, ), if (_backgroundWorkDiagnostics.isSupported) _buildBackgroundDownloadsTile(), - // TODO: "Require authentication to delete downloads" toggle (#1924) - // goes here, backed by local device authentication. // TODO: "Remove orphaned downloads" toggle (#1413) goes here, next to // autoRemoveWatchedDownloads. ], diff --git a/lib/screens/video_player/parts/build.dart b/lib/screens/video_player/parts/build.dart index f82000011..9e1664462 100644 --- a/lib/screens/video_player/parts/build.dart +++ b/lib/screens/video_player/parts/build.dart @@ -142,6 +142,8 @@ extension _VideoPlayerBuildMethods on VideoPlayerScreenState { } void _startMobileZoomGesture() { + // Pinch-to-zoom is one of the optional touch gestures (#1810). + if (!SettingsService.instance.read(SettingsService.gesturePinchToZoom)) return; final filterManager = _videoFilterManager; if (filterManager == null || _isPinchZooming) return; diff --git a/lib/screens/video_player/parts/playback_prompts.dart b/lib/screens/video_player/parts/playback_prompts.dart index 64538d34f..14bacaf61 100644 --- a/lib/screens/video_player/parts/playback_prompts.dart +++ b/lib/screens/video_player/parts/playback_prompts.dart @@ -90,15 +90,18 @@ extension _VideoPlayerPlaybackPromptMethods on VideoPlayerScreenState { final settings = await SettingsService.getInstance(); if (!mounted) return; final autoPlayEnabled = settings.read(SettingsService.autoPlayNextEpisode); + final countdownSeconds = settings.read(SettingsService.playNextCountdown); - if (skipAutoPlayCountdown && autoPlayEnabled) { + // A zero countdown (#1827) behaves like the PiP path above: no prompt, + // straight into the next episode. + if (autoPlayEnabled && (skipAutoPlayCountdown || countdownSeconds == 0)) { unawaited(_playNext()); return; } _setPlayerState(() { _episode.showPlayNextDialog = true; - _episode.autoPlayCountdown.value = autoPlayEnabled ? 5 : -1; + _episode.autoPlayCountdown.value = autoPlayEnabled ? countdownSeconds : -1; }); // Auto-focus Play Next button on TV when dialog appears (only in keyboard/TV mode) @@ -176,6 +179,9 @@ extension _VideoPlayerPlaybackPromptMethods on VideoPlayerScreenState { _setPlayerState(() { _episode.showPlayNextDialog = true; + // Deliberately not [SettingsService.playNextCountdown]: this countdown + // spaces transient-failure retries (see completion_latch.dart), so a + // user preference of 0 must not collapse it into a hot retry loop. _episode.autoPlayCountdown.value = countdown ? 5 : -1; }); diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index d6ec76c3f..4cc9c6c83 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -1346,6 +1346,18 @@ class VideoPlayerScreenState extends State with WidgetsBindin } await currentPlayer.setProperty('hwdec', _getHwdecValue(enableHardwareDecoding)); + // Deinterlacing (#2149) is mpv-only by design — ExoPlayer has no filter + // chain. `auto` deinterlaces only content flagged interlaced. Wrapped: + // a preference must never abort player initialization (an older core + // that rejects `auto` just keeps its default). + if (!(Platform.isAndroid && useExoPlayer) && settingsService.read(SettingsService.deinterlace)) { + try { + await currentPlayer.setProperty('deinterlace', 'auto'); + } catch (e) { + appLogger.w('VideoPlayerScreen: deinterlace not applied', error: e); + } + } + // Subtitle styling is a preference, never a reason to fail playback. // mpv 0.40's OPT_COLOR parser accepts only #RRGGBB/#AARRGGBB (or // r/g/b/a floats), so a stored colour that does not parse would make diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 29b4f9757..0172d1221 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -540,6 +540,27 @@ class SettingsService extends BaseSharedPreferencesService { static const musicVolume = DoublePref('music_volume', defaultValue: 100.0); static const autoPlayNextEpisode = BoolPref('auto_play_next_episode', defaultValue: true); + /// Seconds the Play Next prompt counts down before auto-advancing (#1827). + /// 0 skips the prompt entirely and starts the next episode immediately. + /// Only consulted while [autoPlayNextEpisode] is on. + static final playNextCountdown = IntPref('play_next_countdown', defaultValue: 5, transform: (v) => v.clamp(0, 30)); + + /// Touch gestures on the player surface (#1810). Each defaults on; the + /// edge swipes gate [MobileEdgeAdjustmentTracker] tracking and the zoom + /// pref gates the pinch recognizer in the player screen. + static const gestureBrightnessSwipe = BoolPref('gesture_brightness_swipe', defaultValue: true); + static const gestureVolumeSwipe = BoolPref('gesture_volume_swipe', defaultValue: true); + static const gesturePinchToZoom = BoolPref('gesture_pinch_to_zoom', defaultValue: true); + + /// Deinterlace interlaced video via mpv's `deinterlace=auto` (#2149). + /// mpv-only by design: ExoPlayer has no filter chain. + static const deinterlace = BoolPref('deinterlace'); + + /// Remembered state of the player's always-on-top toggle (#931). The window + /// flag itself is only held while a player is open; this pref re-applies it + /// on the next playback (including autoplay episode transitions). + static const playerAlwaysOnTop = BoolPref('player_always_on_top'); + /// Where Specials (season 0) land in the episode watch order (#1416/#1952). /// Consumed by [sortEpisodesByWatchOrder] (Jellyfin online queue, offline /// next/prev, download/sync "next N", offline OnDeck, Plex fallback queue) @@ -1089,6 +1110,12 @@ class SettingsService extends BaseSharedPreferencesService { dvConversionMode, musicVolume, autoPlayNextEpisode, + playNextCountdown, + gestureBrightnessSwipe, + gestureVolumeSwipe, + gesturePinchToZoom, + deinterlace, + playerAlwaysOnTop, specialsOrdering, useExoPlayer, startupSection, diff --git a/lib/widgets/video_controls/helpers/mobile_edge_adjustment_tracker.dart b/lib/widgets/video_controls/helpers/mobile_edge_adjustment_tracker.dart index cec8e71e8..6f0884eef 100644 --- a/lib/widgets/video_controls/helpers/mobile_edge_adjustment_tracker.dart +++ b/lib/widgets/video_controls/helpers/mobile_edge_adjustment_tracker.dart @@ -68,14 +68,24 @@ class MobileEdgeAdjustmentTracker { bool get isActive => _active; MobileEdgeAdjustmentSide? get side => _side; - MobileEdgeAdjustmentEvent pointerDown(int pointer, Offset position, Size size) { + /// [isSideEnabled] filters which edges may start a gesture (#1810). The + /// pointer is still registered either way so multi-touch chord blocking + /// keeps seeing every finger. + MobileEdgeAdjustmentEvent pointerDown( + int pointer, + Offset position, + Size size, { + bool Function(MobileEdgeAdjustmentSide side)? isSideEnabled, + }) { _activePointers.add(pointer); if (_blockedUntilAllPointersUp) return const MobileEdgeAdjustmentEvent.none(); if (_activePointers.length > 1) return _cancelTracking(clearPointers: false, blockUntilAllPointersUp: true); if (_trackedPointer != null) return const MobileEdgeAdjustmentEvent.none(); final side = mobileEdgeAdjustmentZoneForPosition(position: position, size: size); - if (side == null) return const MobileEdgeAdjustmentEvent.none(); + if (side == null || (isSideEnabled != null && !isSideEnabled(side))) { + return const MobileEdgeAdjustmentEvent.none(); + } _trackedPointer = pointer; _startPosition = position; diff --git a/lib/widgets/video_controls/parts/playback_input.dart b/lib/widgets/video_controls/parts/playback_input.dart index 39bad39f3..d433a8ee6 100644 --- a/lib/widgets/video_controls/parts/playback_input.dart +++ b/lib/widgets/video_controls/parts/playback_input.dart @@ -279,7 +279,12 @@ extension _PlexVideoControlsPlaybackInputMethods on _PlexVideoControlsState { final hit = _edgeAdjustmentSurfaceHit(event.position); _handleEdgeAdjustmentEvent( _mobileTouchGesturesAllowed && hit != null - ? _edgeAdjustmentTracker.pointerDown(event.pointer, hit.position, hit.size) + ? _edgeAdjustmentTracker.pointerDown( + event.pointer, + hit.position, + hit.size, + isSideEnabled: _edgeAdjustmentGestureEnabled, + ) : const MobileEdgeAdjustmentEvent.none(), ); } @@ -347,12 +352,22 @@ extension _PlexVideoControlsPlaybackInputMethods on _PlexVideoControlsState { return (position: renderObject.globalToLocal(globalPosition), size: renderObject.size); } + /// Whether a global touch position sits in an edge zone that can actually + /// start a gesture — a disabled swipe (#1810) must not keep stealing the + /// content-strip drag. bool _isGlobalPositionInEdgeAdjustmentZone(Offset globalPosition) { final hit = _edgeAdjustmentSurfaceHit(globalPosition); if (hit == null) return false; - return mobileEdgeAdjustmentZoneForPosition(position: hit.position, size: hit.size) != null; + final side = mobileEdgeAdjustmentZoneForPosition(position: hit.position, size: hit.size); + return side != null && _edgeAdjustmentGestureEnabled(side); } + /// Left edge adjusts brightness, right edge volume — mirror that split for + /// the per-gesture prefs. + bool _edgeAdjustmentGestureEnabled(MobileEdgeAdjustmentSide side) => SettingsService.instance.read( + side == MobileEdgeAdjustmentSide.left ? SettingsService.gestureBrightnessSwipe : SettingsService.gestureVolumeSwipe, + ); + void _refreshDeviceAdjustmentValues() { unawaited(_readEdgeAdjustmentValue(MobileEdgeAdjustmentSide.left)); unawaited(_readEdgeAdjustmentValue(MobileEdgeAdjustmentSide.right)); diff --git a/lib/widgets/video_controls/parts/visibility.dart b/lib/widgets/video_controls/parts/visibility.dart index 7c376ee6e..57b521ca8 100644 --- a/lib/widgets/video_controls/parts/visibility.dart +++ b/lib/widgets/video_controls/parts/visibility.dart @@ -191,9 +191,15 @@ extension _PlexVideoControlsVisibilityMethods on _PlexVideoControlsState { await FullscreenStateManager().toggleFullscreen(); } - /// Initialize always-on-top state from window manager (desktop only) + /// Initialize always-on-top state (desktop only). The toggle is remembered + /// across player sessions via [SettingsService.playerAlwaysOnTop] (#931) — + /// including episode transitions, which rebuild these controls — while the + /// window flag itself is only held while a player is open (dispose drops + /// the flag without touching the pref). Future _initAlwaysOnTopState() async { - final isOnTop = await windowManager.isAlwaysOnTop(); + final remembered = SettingsService.instance.read(SettingsService.playerAlwaysOnTop); + if (remembered) await windowManager.setAlwaysOnTop(true); + final isOnTop = remembered || await windowManager.isAlwaysOnTop(); if (mounted && isOnTop != _isAlwaysOnTop) { _setControlsState(() { _isAlwaysOnTop = isOnTop; @@ -207,6 +213,7 @@ extension _PlexVideoControlsVisibilityMethods on _PlexVideoControlsState { final newValue = !_isAlwaysOnTop; await windowManager.setAlwaysOnTop(newValue); + unawaited(SettingsService.instance.write(SettingsService.playerAlwaysOnTop, newValue)); if (!mounted) return; _setControlsState(() { _isAlwaysOnTop = newValue; diff --git a/test/screens/settings/playback_settings_screen_test.dart b/test/screens/settings/playback_settings_screen_test.dart index 0f8dbf216..6c8a18dc1 100644 --- a/test/screens/settings/playback_settings_screen_test.dart +++ b/test/screens/settings/playback_settings_screen_test.dart @@ -68,4 +68,93 @@ void main() { expect(settings.prefs.getString(SettingsService.shaderPresetScope.key), 'library'); expect(find.descendant(of: tile, matching: find.textContaining('Per library')), findsOneWidget); }); + + testWidgets('changes the play next countdown and labels zero as immediate', (tester) async { + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = const Size(1000, 1400); + addTearDown(tester.view.resetDevicePixelRatio); + addTearDown(tester.view.resetPhysicalSize); + + await tester.pumpWidget(MaterialApp(theme: monoTheme(dark: true), home: const PlaybackSettingsScreen())); + await tester.pumpAndSettle(); + + final title = find.text('Play Next Countdown'); + await tester.scrollUntilVisible(title, 500, scrollable: find.byType(Scrollable).first); + await tester.ensureVisible(title); + await tester.pumpAndSettle(); + + await tester.tap(title); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextField), '0'); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + + final settings = SettingsService.instance; + expect(settings.read(SettingsService.playNextCountdown), 0); + final tile = find.widgetWithText(ListTile, 'Play Next Countdown'); + expect(find.descendant(of: tile, matching: find.text('Play immediately')), findsOneWidget); + }); + + testWidgets('toggles deinterlacing on the mpv path', (tester) async { + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = const Size(1000, 1400); + addTearDown(tester.view.resetDevicePixelRatio); + addTearDown(tester.view.resetPhysicalSize); + + await tester.pumpWidget(MaterialApp(theme: monoTheme(dark: true), home: const PlaybackSettingsScreen())); + await tester.pumpAndSettle(); + + final title = find.text('Deinterlacing'); + await tester.scrollUntilVisible(title, 500, scrollable: find.byType(Scrollable).first); + await tester.ensureVisible(title); + await tester.pumpAndSettle(); + await tester.tap(title); + await tester.pumpAndSettle(); + + expect(SettingsService.instance.read(SettingsService.deinterlace), isTrue); + }); + + testWidgets('gesture toggles render on mobile and persist', (tester) async { + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = const Size(1000, 1400); + addTearDown(tester.view.resetDevicePixelRatio); + addTearDown(tester.view.resetPhysicalSize); + + await tester.pumpWidget( + MaterialApp( + theme: monoTheme(dark: true).copyWith(platform: TargetPlatform.android), + home: const PlaybackSettingsScreen(), + ), + ); + await tester.pumpAndSettle(); + + final title = find.text('Volume Swipe'); + await tester.scrollUntilVisible(title, 500, scrollable: find.byType(Scrollable).first); + await tester.ensureVisible(title); + await tester.pumpAndSettle(); + await tester.tap(title); + await tester.pumpAndSettle(); + + final settings = SettingsService.instance; + expect(settings.read(SettingsService.gestureVolumeSwipe), isFalse); + expect(settings.read(SettingsService.gestureBrightnessSwipe), isTrue); + expect(settings.read(SettingsService.gesturePinchToZoom), isTrue); + }); + + testWidgets('gesture toggles stay hidden on non-mobile layouts', (tester) async { + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = const Size(1000, 1400); + addTearDown(tester.view.resetDevicePixelRatio); + addTearDown(tester.view.resetPhysicalSize); + + await tester.pumpWidget( + MaterialApp( + theme: monoTheme(dark: true).copyWith(platform: TargetPlatform.macOS), + home: const PlaybackSettingsScreen(), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Gestures'), findsNothing); + }); } diff --git a/test/widgets/video_controls_mobile_edge_adjustment_tracker_test.dart b/test/widgets/video_controls_mobile_edge_adjustment_tracker_test.dart index 11296813a..6935fa622 100644 --- a/test/widgets/video_controls_mobile_edge_adjustment_tracker_test.dart +++ b/test/widgets/video_controls_mobile_edge_adjustment_tracker_test.dart @@ -34,6 +34,43 @@ void main() { expect(event.deltaFraction, greaterThan(0)); }); + test('a disabled side never becomes a candidate', () { + final tracker = MobileEdgeAdjustmentTracker(verticalSlop: 10, verticalDominance: 1.5); + final event = tracker.pointerDown( + 1, + const Offset(40, 300), + size, + isSideEnabled: (side) => side != MobileEdgeAdjustmentSide.left, + ); + expect(event.type, MobileEdgeAdjustmentEventType.none); + expect(tracker.pointerMove(1, const Offset(40, 200)).type, MobileEdgeAdjustmentEventType.none); + }); + + test('a pointer on a disabled side still blocks the enabled side as a second finger', () { + final tracker = MobileEdgeAdjustmentTracker(verticalSlop: 10, verticalDominance: 1.5); + bool onlyRight(MobileEdgeAdjustmentSide side) => side == MobileEdgeAdjustmentSide.right; + + expect( + tracker.pointerDown(1, const Offset(40, 300), size, isSideEnabled: onlyRight).type, + MobileEdgeAdjustmentEventType.none, + ); + // The disabled-side finger is still down, so a second finger on the + // enabled edge is a chord: nothing tracks and it can never activate. + expect( + tracker.pointerDown(2, const Offset(960, 300), size, isSideEnabled: onlyRight).type, + MobileEdgeAdjustmentEventType.none, + ); + expect(tracker.pointerMove(2, const Offset(960, 200)).type, MobileEdgeAdjustmentEventType.none); + tracker.pointerUp(1, const Offset(40, 300)); + tracker.pointerUp(2, const Offset(960, 300)); + + // With all fingers lifted the enabled side tracks normally again. + expect( + tracker.pointerDown(3, const Offset(960, 300), size, isSideEnabled: onlyRight).type, + MobileEdgeAdjustmentEventType.candidate, + ); + }); + test('cancels horizontal movement from the edge', () { final tracker = MobileEdgeAdjustmentTracker(verticalSlop: 10, verticalDominance: 1.5); tracker.pointerDown(1, const Offset(40, 300), size);