From 4083837c3b3b50a991569b21c6310986d74c6bc4 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 24 Feb 2026 05:01:12 +0100 Subject: [PATCH] feat: still watching prompt on sleep timer expiry --- lib/i18n/de.i18n.json | 3 + lib/i18n/en.i18n.json | 3 + lib/i18n/es.i18n.json | 3 + lib/i18n/fr.i18n.json | 3 + lib/i18n/it.i18n.json | 3 + lib/i18n/ko.i18n.json | 3 + lib/i18n/nl.i18n.json | 3 + lib/i18n/strings.g.dart | 4 +- lib/i18n/strings_de.g.dart | 10 +- lib/i18n/strings_en.g.dart | 16 +- lib/i18n/strings_es.g.dart | 10 +- lib/i18n/strings_fr.g.dart | 10 +- lib/i18n/strings_it.g.dart | 10 +- lib/i18n/strings_ko.g.dart | 10 +- lib/i18n/strings_nl.g.dart | 10 +- lib/i18n/strings_sv.g.dart | 10 +- lib/i18n/strings_zh.g.dart | 10 +- lib/i18n/sv.i18n.json | 3 + lib/i18n/zh.i18n.json | 3 + lib/screens/video_player_screen.dart | 226 +++++++++++++++++++++++--- lib/services/sleep_timer_service.dart | 63 ++++++- 21 files changed, 372 insertions(+), 44 deletions(-) diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index cc9f255f..9f89c68d 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -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", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 530b5a0c..a933c1cc 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -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", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index e9742bde..397c8dfd 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -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", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index b0cd23ee..0294c215 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -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", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 53416dad..e6ab50c4 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -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", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 45967dd0..222000fe 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -305,6 +305,9 @@ "timerActive": "타이머 활성화됨", "playbackWillPauseIn": "재생이 ${duration} 후에 일시 중지 됩니다", "sleepTimerCompleted": "수면 타이머 완료됨 - 재생이 일시 중지되었습니다", + "stillWatching": "아직 시청 중이신가요?", + "pausingIn": "${seconds}초 후 일시 정지", + "continueWatching": "계속", "autoPlayNext": "다음 자동 재생", "playNext": "다음 재생", "playButton": "재생", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index 0ffb278a..a6332203 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -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", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index e82d8901..a7a5ef34 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -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 diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index f0a03044..3d4c92d7 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -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', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index 0a58865e..5504814c 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -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', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index a5ea246b..b1b76f66 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -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', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index 1ab121b0..f8ea2b40 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -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', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index c308a2f6..f9534833 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -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', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index ee8f53ac..ef1ad3c8 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -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' => '자정', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index 26b3a9e1..d09b3ae9 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -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', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index 6b7268ef..02507090 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -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', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index ee53097e..385b4941 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -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' => '午夜', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 82759617..c6a29542 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -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", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index c5dcf7b5..c7fa0dcc 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -305,6 +305,9 @@ "timerActive": "定时器已激活", "playbackWillPauseIn": "播放将在 ${duration} 后暂停", "sleepTimerCompleted": "睡眠定时器已完成 - 播放已暂停", + "stillWatching": "还在看吗?", + "pausingIn": "${seconds}秒后暂停", + "continueWatching": "继续", "autoPlayNext": "自动播放下一集", "playNext": "播放下一集", "playButton": "播放", diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index e18738a4..f1214be5 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -160,6 +160,13 @@ class VideoPlayerScreenState extends State 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 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 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 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 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 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 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 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 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 _applyTrackSelection() async { if (!mounted || player == null) return; @@ -2069,12 +2170,7 @@ class VideoPlayerScreenState extends State 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 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 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 with WidgetsBindin ); }, ), + // "Still watching?" overlay (hidden in PiP mode) + ValueListenableBuilder( + valueListenable: PipService().isPipActive, + builder: (context, isInPip, child) { + if (isInPip || !_showStillWatchingPrompt) { + return const SizedBox.shrink(); + } + return ValueListenableBuilder( + 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( diff --git a/lib/services/sleep_timer_service.dart b/lib/services/sleep_timer_service.dart index f0b22608..3594ace4 100644 --- a/lib/services/sleep_timer_service.dart +++ b/lib/services/sleep_timer_service.dart @@ -12,12 +12,18 @@ class SleepTimerService extends ChangeNotifier { Timer? _timer; DateTime? _endTime; Duration? _duration; + Duration? _originalDuration; VoidCallback? _onTimerComplete; + bool _needsRestart = false; final StreamController _completedController = StreamController.broadcast(); + final StreamController _promptController = StreamController.broadcast(); /// Emits when the sleep timer completes (not when cancelled) Stream get onCompleted => _completedController.stream; + /// Emits when the timer fires and wants to show a "still watching?" prompt + Stream 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(); } }