feat: still watching prompt on sleep timer expiry
This commit is contained in:
@@ -305,6 +305,9 @@
|
||||
"timerActive": "Schlaftimer aktiv",
|
||||
"playbackWillPauseIn": "Wiedergabe wird in ${duration} pausiert",
|
||||
"sleepTimerCompleted": "Schlaftimer abgelaufen – Wiedergabe pausiert",
|
||||
"stillWatching": "Schaust du noch?",
|
||||
"pausingIn": "Pause in ${seconds}s",
|
||||
"continueWatching": "Weiter",
|
||||
"autoPlayNext": "Nächstes automatisch abspielen",
|
||||
"playNext": "Nächstes abspielen",
|
||||
"playButton": "Wiedergeben",
|
||||
|
||||
@@ -305,6 +305,9 @@
|
||||
"timerActive": "Timer Active",
|
||||
"playbackWillPauseIn": "Playback will pause in ${duration}",
|
||||
"sleepTimerCompleted": "Sleep timer completed - playback paused",
|
||||
"stillWatching": "Still watching?",
|
||||
"pausingIn": "Pausing in ${seconds}s",
|
||||
"continueWatching": "Continue",
|
||||
"autoPlayNext": "Auto-Play Next",
|
||||
"playNext": "Play Next",
|
||||
"playButton": "Play",
|
||||
|
||||
@@ -305,6 +305,9 @@
|
||||
"timerActive": "Temporizador Activo",
|
||||
"playbackWillPauseIn": "La reproducción se pausará en ${duration}",
|
||||
"sleepTimerCompleted": "Temporizador completado - reproducción pausada",
|
||||
"stillWatching": "¿Sigues viendo?",
|
||||
"pausingIn": "Pausa en ${seconds}s",
|
||||
"continueWatching": "Continuar",
|
||||
"autoPlayNext": "Reproducir siguiente automáticamente",
|
||||
"playNext": "Reproducir siguiente",
|
||||
"playButton": "Reproducir",
|
||||
|
||||
@@ -305,6 +305,9 @@
|
||||
"timerActive": "Minuterie active",
|
||||
"playbackWillPauseIn": "La lecture sera mise en pause dans ${duration}",
|
||||
"sleepTimerCompleted": "Minuterie de mise en veille terminée - lecture en pause",
|
||||
"stillWatching": "Toujours en train de regarder ?",
|
||||
"pausingIn": "Pause dans ${seconds}s",
|
||||
"continueWatching": "Continuer",
|
||||
"autoPlayNext": "Lecture automatique suivante",
|
||||
"playNext": "Lire l'épisode suivant",
|
||||
"playButton": "Lire",
|
||||
|
||||
@@ -305,6 +305,9 @@
|
||||
"timerActive": "Timer attivo",
|
||||
"playbackWillPauseIn": "La riproduzione si interromperà tra ${duration}",
|
||||
"sleepTimerCompleted": "Timer di spegnimento completato - riproduzione in pausa",
|
||||
"stillWatching": "Stai ancora guardando?",
|
||||
"pausingIn": "Pausa tra ${seconds}s",
|
||||
"continueWatching": "Continua",
|
||||
"autoPlayNext": "Riproduzione automatica successivo",
|
||||
"playNext": "Riproduci successivo",
|
||||
"playButton": "Riproduci",
|
||||
|
||||
@@ -305,6 +305,9 @@
|
||||
"timerActive": "타이머 활성화됨",
|
||||
"playbackWillPauseIn": "재생이 ${duration} 후에 일시 중지 됩니다",
|
||||
"sleepTimerCompleted": "수면 타이머 완료됨 - 재생이 일시 중지되었습니다",
|
||||
"stillWatching": "아직 시청 중이신가요?",
|
||||
"pausingIn": "${seconds}초 후 일시 정지",
|
||||
"continueWatching": "계속",
|
||||
"autoPlayNext": "다음 자동 재생",
|
||||
"playNext": "다음 재생",
|
||||
"playButton": "재생",
|
||||
|
||||
@@ -305,6 +305,9 @@
|
||||
"timerActive": "Timer actief",
|
||||
"playbackWillPauseIn": "Afspelen wordt gepauzeerd over ${duration}",
|
||||
"sleepTimerCompleted": "Slaaptimer voltooid - afspelen gepauzeerd",
|
||||
"stillWatching": "Kijk je nog?",
|
||||
"pausingIn": "Pauze over ${seconds}s",
|
||||
"continueWatching": "Doorgaan",
|
||||
"autoPlayNext": "Automatisch volgende afspelen",
|
||||
"playNext": "Volgende afspelen",
|
||||
"playButton": "Afspelen",
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 9
|
||||
/// Strings: 7254 (806 per locale)
|
||||
/// Strings: 7281 (809 per locale)
|
||||
///
|
||||
/// Built on 2026-02-23 at 04:30 UTC
|
||||
/// Built on 2026-02-24 at 03:45 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
||||
@@ -460,6 +460,9 @@ class _TranslationsVideoControlsDe implements TranslationsVideoControlsEn {
|
||||
@override String get timerActive => 'Schlaftimer aktiv';
|
||||
@override String playbackWillPauseIn({required Object duration}) => 'Wiedergabe wird in ${duration} pausiert';
|
||||
@override String get sleepTimerCompleted => 'Schlaftimer abgelaufen – Wiedergabe pausiert';
|
||||
@override String get stillWatching => 'Schaust du noch?';
|
||||
@override String pausingIn({required Object seconds}) => 'Pause in ${seconds}s';
|
||||
@override String get continueWatching => 'Weiter';
|
||||
@override String get autoPlayNext => 'Nächstes automatisch abspielen';
|
||||
@override String get playNext => 'Nächstes abspielen';
|
||||
@override String get playButton => 'Wiedergeben';
|
||||
@@ -1580,6 +1583,9 @@ extension on TranslationsDe {
|
||||
'videoControls.timerActive' => 'Schlaftimer aktiv',
|
||||
'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'Wiedergabe wird in ${duration} pausiert',
|
||||
'videoControls.sleepTimerCompleted' => 'Schlaftimer abgelaufen – Wiedergabe pausiert',
|
||||
'videoControls.stillWatching' => 'Schaust du noch?',
|
||||
'videoControls.pausingIn' => ({required Object seconds}) => 'Pause in ${seconds}s',
|
||||
'videoControls.continueWatching' => 'Weiter',
|
||||
'videoControls.autoPlayNext' => 'Nächstes automatisch abspielen',
|
||||
'videoControls.playNext' => 'Nächstes abspielen',
|
||||
'videoControls.playButton' => 'Wiedergeben',
|
||||
@@ -1812,11 +1818,11 @@ extension on TranslationsDe {
|
||||
'liveTv.live' => 'LIVE',
|
||||
'liveTv.hd' => 'HD',
|
||||
'liveTv.premiere' => 'NEU',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.reloadGuide' => 'Programmführer neu laden',
|
||||
'liveTv.guideReloaded' => 'Programmdaten neu geladen',
|
||||
'liveTv.allChannels' => 'Alle Kanäle',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.now' => 'Jetzt',
|
||||
'liveTv.today' => 'Heute',
|
||||
'liveTv.midnight' => 'Mitternacht',
|
||||
|
||||
@@ -981,6 +981,15 @@ class TranslationsVideoControlsEn {
|
||||
/// en: 'Sleep timer completed - playback paused'
|
||||
String get sleepTimerCompleted => 'Sleep timer completed - playback paused';
|
||||
|
||||
/// en: 'Still watching?'
|
||||
String get stillWatching => 'Still watching?';
|
||||
|
||||
/// en: 'Pausing in ${seconds}s'
|
||||
String pausingIn({required Object seconds}) => 'Pausing in ${seconds}s';
|
||||
|
||||
/// en: 'Continue'
|
||||
String get continueWatching => 'Continue';
|
||||
|
||||
/// en: 'Auto-Play Next'
|
||||
String get autoPlayNext => 'Auto-Play Next';
|
||||
|
||||
@@ -3199,6 +3208,9 @@ extension on Translations {
|
||||
'videoControls.timerActive' => 'Timer Active',
|
||||
'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'Playback will pause in ${duration}',
|
||||
'videoControls.sleepTimerCompleted' => 'Sleep timer completed - playback paused',
|
||||
'videoControls.stillWatching' => 'Still watching?',
|
||||
'videoControls.pausingIn' => ({required Object seconds}) => 'Pausing in ${seconds}s',
|
||||
'videoControls.continueWatching' => 'Continue',
|
||||
'videoControls.autoPlayNext' => 'Auto-Play Next',
|
||||
'videoControls.playNext' => 'Play Next',
|
||||
'videoControls.playButton' => 'Play',
|
||||
@@ -3431,11 +3443,11 @@ extension on Translations {
|
||||
'liveTv.live' => 'LIVE',
|
||||
'liveTv.hd' => 'HD',
|
||||
'liveTv.premiere' => 'NEW',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.reloadGuide' => 'Reload Guide',
|
||||
'liveTv.guideReloaded' => 'Guide data reloaded',
|
||||
'liveTv.allChannels' => 'All Channels',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.now' => 'Now',
|
||||
'liveTv.today' => 'Today',
|
||||
'liveTv.midnight' => 'Midnight',
|
||||
|
||||
@@ -460,6 +460,9 @@ class _TranslationsVideoControlsEs implements TranslationsVideoControlsEn {
|
||||
@override String get timerActive => 'Temporizador Activo';
|
||||
@override String playbackWillPauseIn({required Object duration}) => 'La reproducción se pausará en ${duration}';
|
||||
@override String get sleepTimerCompleted => 'Temporizador completado - reproducción pausada';
|
||||
@override String get stillWatching => '¿Sigues viendo?';
|
||||
@override String pausingIn({required Object seconds}) => 'Pausa en ${seconds}s';
|
||||
@override String get continueWatching => 'Continuar';
|
||||
@override String get autoPlayNext => 'Reproducir siguiente automáticamente';
|
||||
@override String get playNext => 'Reproducir siguiente';
|
||||
@override String get playButton => 'Reproducir';
|
||||
@@ -1580,6 +1583,9 @@ extension on TranslationsEs {
|
||||
'videoControls.timerActive' => 'Temporizador Activo',
|
||||
'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'La reproducción se pausará en ${duration}',
|
||||
'videoControls.sleepTimerCompleted' => 'Temporizador completado - reproducción pausada',
|
||||
'videoControls.stillWatching' => '¿Sigues viendo?',
|
||||
'videoControls.pausingIn' => ({required Object seconds}) => 'Pausa en ${seconds}s',
|
||||
'videoControls.continueWatching' => 'Continuar',
|
||||
'videoControls.autoPlayNext' => 'Reproducir siguiente automáticamente',
|
||||
'videoControls.playNext' => 'Reproducir siguiente',
|
||||
'videoControls.playButton' => 'Reproducir',
|
||||
@@ -1812,11 +1818,11 @@ extension on TranslationsEs {
|
||||
'liveTv.live' => 'EN VIVO',
|
||||
'liveTv.hd' => 'HD',
|
||||
'liveTv.premiere' => 'NUEVO',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.reloadGuide' => 'Recargar guía',
|
||||
'liveTv.guideReloaded' => 'Datos de la guía recargados',
|
||||
'liveTv.allChannels' => 'Todos los canales',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.now' => 'Ahora',
|
||||
'liveTv.today' => 'Hoy',
|
||||
'liveTv.midnight' => 'Medianoche',
|
||||
|
||||
@@ -460,6 +460,9 @@ class _TranslationsVideoControlsFr implements TranslationsVideoControlsEn {
|
||||
@override String get timerActive => 'Minuterie active';
|
||||
@override String playbackWillPauseIn({required Object duration}) => 'La lecture sera mise en pause dans ${duration}';
|
||||
@override String get sleepTimerCompleted => 'Minuterie de mise en veille terminée - lecture en pause';
|
||||
@override String get stillWatching => 'Toujours en train de regarder ?';
|
||||
@override String pausingIn({required Object seconds}) => 'Pause dans ${seconds}s';
|
||||
@override String get continueWatching => 'Continuer';
|
||||
@override String get autoPlayNext => 'Lecture automatique suivante';
|
||||
@override String get playNext => 'Lire l\'épisode suivant';
|
||||
@override String get playButton => 'Lire';
|
||||
@@ -1580,6 +1583,9 @@ extension on TranslationsFr {
|
||||
'videoControls.timerActive' => 'Minuterie active',
|
||||
'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'La lecture sera mise en pause dans ${duration}',
|
||||
'videoControls.sleepTimerCompleted' => 'Minuterie de mise en veille terminée - lecture en pause',
|
||||
'videoControls.stillWatching' => 'Toujours en train de regarder ?',
|
||||
'videoControls.pausingIn' => ({required Object seconds}) => 'Pause dans ${seconds}s',
|
||||
'videoControls.continueWatching' => 'Continuer',
|
||||
'videoControls.autoPlayNext' => 'Lecture automatique suivante',
|
||||
'videoControls.playNext' => 'Lire l\'épisode suivant',
|
||||
'videoControls.playButton' => 'Lire',
|
||||
@@ -1812,11 +1818,11 @@ extension on TranslationsFr {
|
||||
'liveTv.live' => 'EN DIRECT',
|
||||
'liveTv.hd' => 'HD',
|
||||
'liveTv.premiere' => 'NOUVEAU',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.reloadGuide' => 'Recharger le guide',
|
||||
'liveTv.guideReloaded' => 'Données du guide rechargées',
|
||||
'liveTv.allChannels' => 'Toutes les chaînes',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.now' => 'Maintenant',
|
||||
'liveTv.today' => 'Aujourd\'hui',
|
||||
'liveTv.midnight' => 'Minuit',
|
||||
|
||||
@@ -460,6 +460,9 @@ class _TranslationsVideoControlsIt implements TranslationsVideoControlsEn {
|
||||
@override String get timerActive => 'Timer attivo';
|
||||
@override String playbackWillPauseIn({required Object duration}) => 'La riproduzione si interromperà tra ${duration}';
|
||||
@override String get sleepTimerCompleted => 'Timer di spegnimento completato - riproduzione in pausa';
|
||||
@override String get stillWatching => 'Stai ancora guardando?';
|
||||
@override String pausingIn({required Object seconds}) => 'Pausa tra ${seconds}s';
|
||||
@override String get continueWatching => 'Continua';
|
||||
@override String get autoPlayNext => 'Riproduzione automatica successivo';
|
||||
@override String get playNext => 'Riproduci successivo';
|
||||
@override String get playButton => 'Riproduci';
|
||||
@@ -1580,6 +1583,9 @@ extension on TranslationsIt {
|
||||
'videoControls.timerActive' => 'Timer attivo',
|
||||
'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'La riproduzione si interromperà tra ${duration}',
|
||||
'videoControls.sleepTimerCompleted' => 'Timer di spegnimento completato - riproduzione in pausa',
|
||||
'videoControls.stillWatching' => 'Stai ancora guardando?',
|
||||
'videoControls.pausingIn' => ({required Object seconds}) => 'Pausa tra ${seconds}s',
|
||||
'videoControls.continueWatching' => 'Continua',
|
||||
'videoControls.autoPlayNext' => 'Riproduzione automatica successivo',
|
||||
'videoControls.playNext' => 'Riproduci successivo',
|
||||
'videoControls.playButton' => 'Riproduci',
|
||||
@@ -1812,11 +1818,11 @@ extension on TranslationsIt {
|
||||
'liveTv.live' => 'IN DIRETTA',
|
||||
'liveTv.hd' => 'HD',
|
||||
'liveTv.premiere' => 'NUOVO',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.reloadGuide' => 'Ricarica guida',
|
||||
'liveTv.guideReloaded' => 'Dati della guida ricaricati',
|
||||
'liveTv.allChannels' => 'Tutti i canali',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.now' => 'Ora',
|
||||
'liveTv.today' => 'Oggi',
|
||||
'liveTv.midnight' => 'Mezzanotte',
|
||||
|
||||
@@ -460,6 +460,9 @@ class _TranslationsVideoControlsKo implements TranslationsVideoControlsEn {
|
||||
@override String get timerActive => '타이머 활성화됨';
|
||||
@override String playbackWillPauseIn({required Object duration}) => '재생이 ${duration} 후에 일시 중지 됩니다';
|
||||
@override String get sleepTimerCompleted => '수면 타이머 완료됨 - 재생이 일시 중지되었습니다';
|
||||
@override String get stillWatching => '아직 시청 중이신가요?';
|
||||
@override String pausingIn({required Object seconds}) => '${seconds}초 후 일시 정지';
|
||||
@override String get continueWatching => '계속';
|
||||
@override String get autoPlayNext => '다음 자동 재생';
|
||||
@override String get playNext => '다음 재생';
|
||||
@override String get playButton => '재생';
|
||||
@@ -1580,6 +1583,9 @@ extension on TranslationsKo {
|
||||
'videoControls.timerActive' => '타이머 활성화됨',
|
||||
'videoControls.playbackWillPauseIn' => ({required Object duration}) => '재생이 ${duration} 후에 일시 중지 됩니다',
|
||||
'videoControls.sleepTimerCompleted' => '수면 타이머 완료됨 - 재생이 일시 중지되었습니다',
|
||||
'videoControls.stillWatching' => '아직 시청 중이신가요?',
|
||||
'videoControls.pausingIn' => ({required Object seconds}) => '${seconds}초 후 일시 정지',
|
||||
'videoControls.continueWatching' => '계속',
|
||||
'videoControls.autoPlayNext' => '다음 자동 재생',
|
||||
'videoControls.playNext' => '다음 재생',
|
||||
'videoControls.playButton' => '재생',
|
||||
@@ -1812,11 +1818,11 @@ extension on TranslationsKo {
|
||||
'liveTv.live' => '실시간',
|
||||
'liveTv.hd' => 'HD',
|
||||
'liveTv.premiere' => '신규',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.reloadGuide' => '편성표 새로고침',
|
||||
'liveTv.guideReloaded' => '편성표 데이터가 새로고침되었습니다',
|
||||
'liveTv.allChannels' => '전체 채널',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.now' => '지금',
|
||||
'liveTv.today' => '오늘',
|
||||
'liveTv.midnight' => '자정',
|
||||
|
||||
@@ -460,6 +460,9 @@ class _TranslationsVideoControlsNl implements TranslationsVideoControlsEn {
|
||||
@override String get timerActive => 'Timer actief';
|
||||
@override String playbackWillPauseIn({required Object duration}) => 'Afspelen wordt gepauzeerd over ${duration}';
|
||||
@override String get sleepTimerCompleted => 'Slaaptimer voltooid - afspelen gepauzeerd';
|
||||
@override String get stillWatching => 'Kijk je nog?';
|
||||
@override String pausingIn({required Object seconds}) => 'Pauze over ${seconds}s';
|
||||
@override String get continueWatching => 'Doorgaan';
|
||||
@override String get autoPlayNext => 'Automatisch volgende afspelen';
|
||||
@override String get playNext => 'Volgende afspelen';
|
||||
@override String get playButton => 'Afspelen';
|
||||
@@ -1580,6 +1583,9 @@ extension on TranslationsNl {
|
||||
'videoControls.timerActive' => 'Timer actief',
|
||||
'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'Afspelen wordt gepauzeerd over ${duration}',
|
||||
'videoControls.sleepTimerCompleted' => 'Slaaptimer voltooid - afspelen gepauzeerd',
|
||||
'videoControls.stillWatching' => 'Kijk je nog?',
|
||||
'videoControls.pausingIn' => ({required Object seconds}) => 'Pauze over ${seconds}s',
|
||||
'videoControls.continueWatching' => 'Doorgaan',
|
||||
'videoControls.autoPlayNext' => 'Automatisch volgende afspelen',
|
||||
'videoControls.playNext' => 'Volgende afspelen',
|
||||
'videoControls.playButton' => 'Afspelen',
|
||||
@@ -1812,11 +1818,11 @@ extension on TranslationsNl {
|
||||
'liveTv.live' => 'LIVE',
|
||||
'liveTv.hd' => 'HD',
|
||||
'liveTv.premiere' => 'NIEUW',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.reloadGuide' => 'Gids herladen',
|
||||
'liveTv.guideReloaded' => 'Gidsgegevens herladen',
|
||||
'liveTv.allChannels' => 'Alle zenders',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.now' => 'Nu',
|
||||
'liveTv.today' => 'Vandaag',
|
||||
'liveTv.midnight' => 'Middernacht',
|
||||
|
||||
@@ -460,6 +460,9 @@ class _TranslationsVideoControlsSv implements TranslationsVideoControlsEn {
|
||||
@override String get timerActive => 'Timer aktiv';
|
||||
@override String playbackWillPauseIn({required Object duration}) => 'Uppspelningen pausas om ${duration}';
|
||||
@override String get sleepTimerCompleted => 'Sovtimer slutförd - uppspelning pausad';
|
||||
@override String get stillWatching => 'Tittar du fortfarande?';
|
||||
@override String pausingIn({required Object seconds}) => 'Pausar om ${seconds}s';
|
||||
@override String get continueWatching => 'Fortsätt';
|
||||
@override String get autoPlayNext => 'Spela nästa automatiskt';
|
||||
@override String get playNext => 'Spela nästa';
|
||||
@override String get playButton => 'Spela';
|
||||
@@ -1580,6 +1583,9 @@ extension on TranslationsSv {
|
||||
'videoControls.timerActive' => 'Timer aktiv',
|
||||
'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'Uppspelningen pausas om ${duration}',
|
||||
'videoControls.sleepTimerCompleted' => 'Sovtimer slutförd - uppspelning pausad',
|
||||
'videoControls.stillWatching' => 'Tittar du fortfarande?',
|
||||
'videoControls.pausingIn' => ({required Object seconds}) => 'Pausar om ${seconds}s',
|
||||
'videoControls.continueWatching' => 'Fortsätt',
|
||||
'videoControls.autoPlayNext' => 'Spela nästa automatiskt',
|
||||
'videoControls.playNext' => 'Spela nästa',
|
||||
'videoControls.playButton' => 'Spela',
|
||||
@@ -1812,11 +1818,11 @@ extension on TranslationsSv {
|
||||
'liveTv.live' => 'LIVE',
|
||||
'liveTv.hd' => 'HD',
|
||||
'liveTv.premiere' => 'NY',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.reloadGuide' => 'Ladda om programguide',
|
||||
'liveTv.guideReloaded' => 'Programdata omladdad',
|
||||
'liveTv.allChannels' => 'Alla kanaler',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.now' => 'Nu',
|
||||
'liveTv.today' => 'Idag',
|
||||
'liveTv.midnight' => 'Midnatt',
|
||||
|
||||
@@ -460,6 +460,9 @@ class _TranslationsVideoControlsZh implements TranslationsVideoControlsEn {
|
||||
@override String get timerActive => '定时器已激活';
|
||||
@override String playbackWillPauseIn({required Object duration}) => '播放将在 ${duration} 后暂停';
|
||||
@override String get sleepTimerCompleted => '睡眠定时器已完成 - 播放已暂停';
|
||||
@override String get stillWatching => '还在看吗?';
|
||||
@override String pausingIn({required Object seconds}) => '${seconds}秒后暂停';
|
||||
@override String get continueWatching => '继续';
|
||||
@override String get autoPlayNext => '自动播放下一集';
|
||||
@override String get playNext => '播放下一集';
|
||||
@override String get playButton => '播放';
|
||||
@@ -1580,6 +1583,9 @@ extension on TranslationsZh {
|
||||
'videoControls.timerActive' => '定时器已激活',
|
||||
'videoControls.playbackWillPauseIn' => ({required Object duration}) => '播放将在 ${duration} 后暂停',
|
||||
'videoControls.sleepTimerCompleted' => '睡眠定时器已完成 - 播放已暂停',
|
||||
'videoControls.stillWatching' => '还在看吗?',
|
||||
'videoControls.pausingIn' => ({required Object seconds}) => '${seconds}秒后暂停',
|
||||
'videoControls.continueWatching' => '继续',
|
||||
'videoControls.autoPlayNext' => '自动播放下一集',
|
||||
'videoControls.playNext' => '播放下一集',
|
||||
'videoControls.playButton' => '播放',
|
||||
@@ -1812,11 +1818,11 @@ extension on TranslationsZh {
|
||||
'liveTv.live' => '直播',
|
||||
'liveTv.hd' => '高清',
|
||||
'liveTv.premiere' => '新',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.reloadGuide' => '重新加载节目指南',
|
||||
'liveTv.guideReloaded' => '节目指南已重新加载',
|
||||
'liveTv.allChannels' => '所有频道',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.now' => '现在',
|
||||
'liveTv.today' => '今天',
|
||||
'liveTv.midnight' => '午夜',
|
||||
|
||||
@@ -305,6 +305,9 @@
|
||||
"timerActive": "Timer aktiv",
|
||||
"playbackWillPauseIn": "Uppspelningen pausas om ${duration}",
|
||||
"sleepTimerCompleted": "Sovtimer slutförd - uppspelning pausad",
|
||||
"stillWatching": "Tittar du fortfarande?",
|
||||
"pausingIn": "Pausar om ${seconds}s",
|
||||
"continueWatching": "Fortsätt",
|
||||
"autoPlayNext": "Spela nästa automatiskt",
|
||||
"playNext": "Spela nästa",
|
||||
"playButton": "Spela",
|
||||
|
||||
@@ -305,6 +305,9 @@
|
||||
"timerActive": "定时器已激活",
|
||||
"playbackWillPauseIn": "播放将在 ${duration} 后暂停",
|
||||
"sleepTimerCompleted": "睡眠定时器已完成 - 播放已暂停",
|
||||
"stillWatching": "还在看吗?",
|
||||
"pausingIn": "${seconds}秒后暂停",
|
||||
"continueWatching": "继续",
|
||||
"autoPlayNext": "自动播放下一集",
|
||||
"playNext": "播放下一集",
|
||||
"playButton": "播放",
|
||||
|
||||
@@ -160,6 +160,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
late final FocusNode _playNextCancelFocusNode;
|
||||
late final FocusNode _playNextConfirmFocusNode;
|
||||
|
||||
// "Still watching?" prompt (sleep timer)
|
||||
bool _showStillWatchingPrompt = false;
|
||||
int _stillWatchingCountdown = 30;
|
||||
Timer? _stillWatchingTimer;
|
||||
late final FocusNode _stillWatchingPauseFocusNode;
|
||||
late final FocusNode _stillWatchingContinueFocusNode;
|
||||
|
||||
// Screen-level focus node: persists across loading/initialized phases so
|
||||
// key events never escape the video player route.
|
||||
late final FocusNode _screenFocusNode;
|
||||
@@ -221,6 +228,10 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
_playNextCancelFocusNode = FocusNode(debugLabel: 'PlayNextCancel');
|
||||
_playNextConfirmFocusNode = FocusNode(debugLabel: 'PlayNextConfirm');
|
||||
|
||||
// Initialize "Still watching?" dialog focus nodes
|
||||
_stillWatchingPauseFocusNode = FocusNode(debugLabel: 'StillWatchingPause');
|
||||
_stillWatchingContinueFocusNode = FocusNode(debugLabel: 'StillWatchingContinue');
|
||||
|
||||
// Screen-level focus node that wraps the entire build output.
|
||||
// Ensures a single stable focus target across loading → initialized phases.
|
||||
_screenFocusNode = FocusNode(debugLabel: 'VideoPlayerScreen');
|
||||
@@ -265,9 +276,9 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
// Wire companion remote playback callbacks
|
||||
_setupCompanionRemoteCallbacks();
|
||||
|
||||
// Exit the player when the sleep timer completes so the device can auto-lock
|
||||
_sleepTimerSubscription = SleepTimerService().onCompleted.listen((_) {
|
||||
if (mounted) _handleBackButton();
|
||||
// Show "Still watching?" prompt when sleep timer fires
|
||||
_sleepTimerSubscription = SleepTimerService().onPrompt.listen((_) {
|
||||
if (mounted) _showStillWatchingDialog();
|
||||
});
|
||||
|
||||
// Initialize player asynchronously with buffer size from settings
|
||||
@@ -463,6 +474,12 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
_isPlayerInitialized = true;
|
||||
});
|
||||
|
||||
// Restart sleep timer if we're starting a new playback session
|
||||
final p = player;
|
||||
if (p != null) {
|
||||
SleepTimerService().restartIfNeeded(() => p.pause());
|
||||
}
|
||||
|
||||
// Enable wakelock to prevent screen from turning off during playback
|
||||
WakelockPlus.enable();
|
||||
appLogger.d('Wakelock enabled for video playback');
|
||||
@@ -1570,6 +1587,11 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
_videoPIPManager?.onBeforeEnterPip = null;
|
||||
_videoFilterManager?.dispose();
|
||||
|
||||
// Mark sleep timer for restart if truly exiting (not episode transition)
|
||||
if (!_isReplacingWithVideo) {
|
||||
SleepTimerService().markNeedsRestart();
|
||||
}
|
||||
|
||||
// Cancel stream subscriptions
|
||||
_playingSubscription?.cancel();
|
||||
_completedSubscription?.cancel();
|
||||
@@ -1585,10 +1607,17 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
// Cancel auto-play timer
|
||||
_autoPlayTimer?.cancel();
|
||||
|
||||
// Cancel still watching timer
|
||||
_stillWatchingTimer?.cancel();
|
||||
|
||||
// Dispose Play Next dialog focus nodes
|
||||
_playNextCancelFocusNode.dispose();
|
||||
_playNextConfirmFocusNode.dispose();
|
||||
|
||||
// Dispose "Still watching?" dialog focus nodes
|
||||
_stillWatchingPauseFocusNode.dispose();
|
||||
_stillWatchingContinueFocusNode.dispose();
|
||||
|
||||
// Dispose screen-level focus node
|
||||
_screenFocusNode.removeListener(_onScreenFocusChanged);
|
||||
_screenFocusNode.dispose();
|
||||
@@ -1678,7 +1707,11 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
}
|
||||
|
||||
void _onVideoCompleted(bool completed) async {
|
||||
if (completed && _nextEpisode != null && !_showPlayNextDialog && !_completionTriggered) {
|
||||
if (completed &&
|
||||
_nextEpisode != null &&
|
||||
!_showPlayNextDialog &&
|
||||
!_showStillWatchingPrompt &&
|
||||
!_completionTriggered) {
|
||||
_completionTriggered = true;
|
||||
|
||||
// Capture keyboard mode before async gap
|
||||
@@ -1743,6 +1776,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
|
||||
// Cancel auto-play timer if running
|
||||
_autoPlayTimer?.cancel();
|
||||
_dismissStillWatching();
|
||||
|
||||
// Notify Watch Together of episode change before navigating
|
||||
_notifyWatchTogetherMediaChange(metadata: _nextEpisode);
|
||||
@@ -1934,6 +1968,73 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
});
|
||||
}
|
||||
|
||||
// -- "Still watching?" prompt --
|
||||
|
||||
void _showStillWatchingDialog() {
|
||||
// Don't show if auto-play dialog is already visible
|
||||
if (_showPlayNextDialog) return;
|
||||
|
||||
final isKeyboardMode = PlatformDetector.isTV() && InputModeTracker.isKeyboardMode(context);
|
||||
|
||||
setState(() {
|
||||
_showStillWatchingPrompt = true;
|
||||
_stillWatchingCountdown = 30;
|
||||
});
|
||||
|
||||
if (isKeyboardMode) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _stillWatchingContinueFocusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
_stillWatchingTimer?.cancel();
|
||||
_stillWatchingTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_stillWatchingCountdown--;
|
||||
});
|
||||
if (_stillWatchingCountdown <= 0) {
|
||||
timer.cancel();
|
||||
_onStillWatchingTimeout();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onStillWatchingTimeout() {
|
||||
player?.pause();
|
||||
setState(() {
|
||||
_showStillWatchingPrompt = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _onStillWatchingContinue() {
|
||||
_stillWatchingTimer?.cancel();
|
||||
SleepTimerService().restartTimer();
|
||||
setState(() {
|
||||
_showStillWatchingPrompt = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _onStillWatchingPause() {
|
||||
_stillWatchingTimer?.cancel();
|
||||
player?.pause();
|
||||
setState(() {
|
||||
_showStillWatchingPrompt = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _dismissStillWatching() {
|
||||
_stillWatchingTimer?.cancel();
|
||||
if (_showStillWatchingPrompt) {
|
||||
setState(() {
|
||||
_showStillWatchingPrompt = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply track selection using the TrackSelectionService
|
||||
Future<void> _applyTrackSelection() async {
|
||||
if (!mounted || player == null) return;
|
||||
@@ -2069,12 +2170,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
}
|
||||
}
|
||||
|
||||
await _saveTrackPreferences(
|
||||
partId: partId,
|
||||
trackType: 'audio',
|
||||
languageCode: track.language,
|
||||
streamID: streamID,
|
||||
);
|
||||
await _saveTrackPreferences(partId: partId, trackType: 'audio', languageCode: track.language, streamID: streamID);
|
||||
}
|
||||
|
||||
/// Handle subtitle track changes from the user - save both stream selection and language preference
|
||||
@@ -2115,12 +2211,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
}
|
||||
}
|
||||
|
||||
await _saveTrackPreferences(
|
||||
partId: partId,
|
||||
trackType: 'subtitle',
|
||||
languageCode: languageCode,
|
||||
streamID: streamID,
|
||||
);
|
||||
await _saveTrackPreferences(partId: partId, trackType: 'subtitle', languageCode: languageCode, streamID: streamID);
|
||||
}
|
||||
|
||||
/// Set flag to skip orientation restoration when replacing with another video
|
||||
@@ -2251,9 +2342,8 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
},
|
||||
child: OverlaySheetHost(
|
||||
child: Builder(
|
||||
builder: (sheetContext) => _isPlayerInitialized && player != null
|
||||
? _buildVideoPlayer(sheetContext)
|
||||
: _buildLoadingSpinner(),
|
||||
builder: (sheetContext) =>
|
||||
_isPlayerInitialized && player != null ? _buildVideoPlayer(sheetContext) : _buildLoadingSpinner(),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -2548,6 +2638,104 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
);
|
||||
},
|
||||
),
|
||||
// "Still watching?" overlay (hidden in PiP mode)
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: PipService().isPipActive,
|
||||
builder: (context, isInPip, child) {
|
||||
if (isInPip || !_showStillWatchingPrompt) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: _controlsVisible,
|
||||
builder: (context, controlsShown, child) {
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
right: 24,
|
||||
bottom: controlsShown ? 100 : 24,
|
||||
child: Container(
|
||||
width: 320,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.9),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
t.videoControls.stillWatching,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.7),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
t.videoControls.pausingIn(seconds: '$_stillWatchingCountdown'),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FocusableButton(
|
||||
focusNode: _stillWatchingPauseFocusNode,
|
||||
onPressed: _onStillWatchingPause,
|
||||
autoScroll: false,
|
||||
onNavigateRight: () => _stillWatchingContinueFocusNode.requestFocus(),
|
||||
onNavigateUp: () {},
|
||||
onNavigateDown: () {},
|
||||
child: OutlinedButton(
|
||||
onPressed: _onStillWatchingPause,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
side: BorderSide(color: Colors.white.withValues(alpha: 0.5)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: Text(t.videoControls.pauseButton),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: FocusableButton(
|
||||
focusNode: _stillWatchingContinueFocusNode,
|
||||
onPressed: _onStillWatchingContinue,
|
||||
autoScroll: false,
|
||||
onNavigateLeft: () => _stillWatchingPauseFocusNode.requestFocus(),
|
||||
onNavigateUp: () {},
|
||||
onNavigateDown: () {},
|
||||
child: FilledButton(
|
||||
onPressed: _onStillWatchingContinue,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('$_stillWatchingCountdown'),
|
||||
const SizedBox(width: 4),
|
||||
Text(t.videoControls.continueWatching),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
// Buffering indicator (also shows during initial load, but not when exiting)
|
||||
// Hidden in PiP mode
|
||||
ValueListenableBuilder<bool>(
|
||||
|
||||
@@ -12,12 +12,18 @@ class SleepTimerService extends ChangeNotifier {
|
||||
Timer? _timer;
|
||||
DateTime? _endTime;
|
||||
Duration? _duration;
|
||||
Duration? _originalDuration;
|
||||
VoidCallback? _onTimerComplete;
|
||||
bool _needsRestart = false;
|
||||
final StreamController<void> _completedController = StreamController<void>.broadcast();
|
||||
final StreamController<void> _promptController = StreamController<void>.broadcast();
|
||||
|
||||
/// Emits when the sleep timer completes (not when cancelled)
|
||||
Stream<void> get onCompleted => _completedController.stream;
|
||||
|
||||
/// Emits when the timer fires and wants to show a "still watching?" prompt
|
||||
Stream<void> get onPrompt => _promptController.stream;
|
||||
|
||||
/// Whether a timer is currently active
|
||||
bool get isActive => _timer != null && _timer!.isActive;
|
||||
|
||||
@@ -27,6 +33,9 @@ class SleepTimerService extends ChangeNotifier {
|
||||
/// The original duration of the timer
|
||||
Duration? get duration => _duration;
|
||||
|
||||
/// The user-selected duration (unmodified by extendTimer)
|
||||
Duration? get originalDuration => _originalDuration;
|
||||
|
||||
/// Remaining time on the timer
|
||||
Duration? get remainingTime {
|
||||
if (_endTime == null) return null;
|
||||
@@ -41,6 +50,7 @@ class SleepTimerService extends ChangeNotifier {
|
||||
// Cancel any existing timer
|
||||
cancelTimer();
|
||||
|
||||
_originalDuration = duration;
|
||||
_duration = duration;
|
||||
_endTime = DateTime.now().add(duration);
|
||||
_onTimerComplete = onComplete;
|
||||
@@ -52,9 +62,9 @@ class SleepTimerService extends ChangeNotifier {
|
||||
final remaining = remainingTime;
|
||||
|
||||
if (remaining == null || remaining.inSeconds <= 0) {
|
||||
appLogger.d('Sleep timer completed');
|
||||
_executeCallback();
|
||||
cancelTimer();
|
||||
appLogger.d('Sleep timer completed - showing prompt');
|
||||
_stopTimerOnly();
|
||||
_promptController.add(null);
|
||||
} else {
|
||||
// Notify listeners to update UI
|
||||
notifyListeners();
|
||||
@@ -64,19 +74,51 @@ class SleepTimerService extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Cancel the active timer
|
||||
/// Cancel the active timer (user-initiated, clears everything)
|
||||
void cancelTimer() {
|
||||
if (_timer != null) {
|
||||
if (_timer != null || _originalDuration != null) {
|
||||
appLogger.d('Sleep timer cancelled');
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
_endTime = null;
|
||||
_duration = null;
|
||||
_originalDuration = null;
|
||||
_onTimerComplete = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Restart the timer with the original user-selected duration
|
||||
void restartTimer() {
|
||||
if (_originalDuration != null && _onTimerComplete != null) {
|
||||
final duration = _originalDuration!;
|
||||
final callback = _onTimerComplete!;
|
||||
startTimer(duration, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute the completion callback directly (fallback path)
|
||||
void executeCompletion() {
|
||||
_executeCallback();
|
||||
}
|
||||
|
||||
/// Mark that the timer should restart when a new playback session begins
|
||||
/// (e.g. user exited the player and started something new)
|
||||
void markNeedsRestart() {
|
||||
if (isActive || _originalDuration != null) {
|
||||
_needsRestart = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Restart the timer if it was marked for restart (new playback session).
|
||||
/// [onComplete] provides the new callback for the fresh session.
|
||||
void restartIfNeeded(VoidCallback onComplete) {
|
||||
if (_needsRestart && _originalDuration != null) {
|
||||
_needsRestart = false;
|
||||
startTimer(_originalDuration!, onComplete);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend the current timer by the specified duration
|
||||
void extendTimer(Duration additionalTime) {
|
||||
if (_endTime != null) {
|
||||
@@ -87,6 +129,16 @@ class SleepTimerService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop the periodic timer but preserve _originalDuration and _onTimerComplete
|
||||
/// for the prompt flow (restart/completion)
|
||||
void _stopTimerOnly() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
_endTime = null;
|
||||
_duration = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _executeCallback() {
|
||||
if (_onTimerComplete != null) {
|
||||
try {
|
||||
@@ -102,6 +154,7 @@ class SleepTimerService extends ChangeNotifier {
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_completedController.close();
|
||||
_promptController.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user