feat(player): add play-next countdown, gesture toggles, and deinterlacing

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
This commit is contained in:
edde746
2026-08-27 16:42:38 +02:00
parent c9de78697b
commit a97be5823f
36 changed files with 568 additions and 55 deletions
+11
View File
@@ -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",
+11
View File
@@ -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": "Къде се записва и откъде се прилага отново промяна, направена по време на възпроизвеждане",
+11
View File
@@ -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",
+11
View File
@@ -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",
+11
View File
@@ -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",
+11
View File
@@ -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",
+11
View File
@@ -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",
+11
View File
@@ -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",
+11
View File
@@ -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",
+11
View File
@@ -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": "再生中に行った変更を保存し、再適用する場所",
+11
View File
@@ -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": "Ойнату кезінде жасалған өзгеріс сақталатын және қайта қолданылатын орын",
+11
View File
@@ -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": "재생 중 변경한 사항을 저장하고 다시 적용할 위치",
+11
View File
@@ -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",
+11
View File
@@ -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",
+11
View File
@@ -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",
+11
View File
@@ -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",
+11
View File
@@ -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": "Где сохраняется и откуда повторно применяется изменение, сделанное во время воспроизведения",
+1 -1
View File
@@ -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
+50 -6
View File
@@ -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.',
+11
View File
@@ -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",
+11
View File
@@ -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",
+11
View File
@@ -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",
+11
View File
@@ -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": "播放期間所做的變更要儲存並從何處重新套用",
+11
View File
@@ -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": "播放期间所做的更改保存并重新应用的位置",
@@ -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),
@@ -66,9 +66,6 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
(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<PlaybackSettingsScreen> {
],
),
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<PlaybackSettingsScreen> {
_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<PlaybackSettingsScreen> {
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<PlaybackSettingsScreen> {
],
);
/// 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<bool>(
pref: SettingsService.useExoPlayer,
icon: Symbols.play_circle_rounded,
@@ -436,6 +464,13 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
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,
@@ -435,8 +435,6 @@ class _SettingsScreenState extends State<SettingsScreen> 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.
],
@@ -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;
@@ -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;
});
+12
View File
@@ -1346,6 +1346,18 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> 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
+27
View File
@@ -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,
@@ -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;
@@ -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));
@@ -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<void> _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;
@@ -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);
});
}
@@ -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);