From 294996eac1cbeebbec0f8673fa023ae4059a5a59 Mon Sep 17 00:00:00 2001 From: Timo Kox Date: Sat, 30 May 2026 07:16:01 +0200 Subject: [PATCH] feat(player): add end-of-video sleep timer option (#1111) close #540 --- lib/i18n/bg.i18n.json | 4 + lib/i18n/da.i18n.json | 4 + lib/i18n/de.i18n.json | 4 + lib/i18n/en.i18n.json | 4 + lib/i18n/es.i18n.json | 4 + lib/i18n/fr.i18n.json | 4 + lib/i18n/it.i18n.json | 4 + lib/i18n/ja.i18n.json | 4 + lib/i18n/ko.i18n.json | 4 + lib/i18n/nb.i18n.json | 4 + lib/i18n/nl.i18n.json | 4 + lib/i18n/pl.i18n.json | 4 + lib/i18n/pt.i18n.json | 4 + lib/i18n/ru.i18n.json | 4 + lib/i18n/strings.g.dart | 2 +- lib/i18n/strings_bg.g.dart | 16 +- lib/i18n/strings_da.g.dart | 16 +- lib/i18n/strings_de.g.dart | 16 +- lib/i18n/strings_en.g.dart | 24 ++- lib/i18n/strings_es.g.dart | 16 +- lib/i18n/strings_fr.g.dart | 16 +- lib/i18n/strings_it.g.dart | 16 +- lib/i18n/strings_ja.g.dart | 16 +- lib/i18n/strings_ko.g.dart | 16 +- lib/i18n/strings_nb.g.dart | 16 +- lib/i18n/strings_nl.g.dart | 16 +- lib/i18n/strings_pl.g.dart | 16 +- lib/i18n/strings_pt.g.dart | 16 +- lib/i18n/strings_ru.g.dart | 16 +- lib/i18n/strings_sv.g.dart | 16 +- lib/i18n/strings_zh.g.dart | 16 +- lib/i18n/sv.i18n.json | 4 + lib/i18n/zh.i18n.json | 4 + .../video_player/parts/playback_prompts.dart | 9 ++ lib/services/sleep_timer_service.dart | 47 +++++- .../widgets/sleep_timer_active_status.dart | 48 ++++-- .../widgets/sleep_timer_content.dart | 145 +++++++++++++++++- .../widgets/sleep_timer_duration_list.dart | 54 ------- test/services/sleep_timer_service_test.dart | 114 ++++++++++++++ 39 files changed, 604 insertions(+), 143 deletions(-) delete mode 100644 lib/widgets/video_controls/widgets/sleep_timer_duration_list.dart diff --git a/lib/i18n/bg.i18n.json b/lib/i18n/bg.i18n.json index 6cc1231a..1b6f977a 100644 --- a/lib/i18n/bg.i18n.json +++ b/lib/i18n/bg.i18n.json @@ -407,6 +407,10 @@ "unlockRotation": "Отключи ротацията", "timerActive": "Таймерът е активен", "playbackWillPauseIn": "Възпроизвеждането ще спре след ${duration}", + "sleepTimerEndOfVideo": "Край на текущото видео", + "sleepTimerStopAtHeader": "Спиране при", + "sleepTimerDurationHeader": "Таймер", + "playbackWillPauseAtEnd": "Възпроизвеждането ще спре в края на това видео", "stillWatching": "Още ли гледате?", "pausingIn": "Пауза след ${seconds}сек", "continueWatching": "Продължи", diff --git a/lib/i18n/da.i18n.json b/lib/i18n/da.i18n.json index 02875b9c..025e1c4d 100644 --- a/lib/i18n/da.i18n.json +++ b/lib/i18n/da.i18n.json @@ -407,6 +407,10 @@ "unlockRotation": "Lås rotation op", "timerActive": "Timer aktiv", "playbackWillPauseIn": "Afspilning pauses om ${duration}", + "sleepTimerEndOfVideo": "Slutningen af aktuel video", + "sleepTimerStopAtHeader": "Stop ved", + "sleepTimerDurationHeader": "Timer", + "playbackWillPauseAtEnd": "Afspilning pauses i slutningen af denne video", "stillWatching": "Ser du stadig?", "pausingIn": "Pauser om ${seconds}s", "continueWatching": "Fortsæt", diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index ca0baac6..2c561f72 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -407,6 +407,10 @@ "unlockRotation": "Rotation entsperren", "timerActive": "Schlaftimer aktiv", "playbackWillPauseIn": "Wiedergabe wird in ${duration} pausiert", + "sleepTimerEndOfVideo": "Ende des aktuellen Videos", + "sleepTimerStopAtHeader": "Beenden bei", + "sleepTimerDurationHeader": "Timer", + "playbackWillPauseAtEnd": "Wiedergabe wird am Ende dieses Videos pausiert", "stillWatching": "Schaust du noch?", "pausingIn": "Pause in ${seconds}s", "continueWatching": "Weiter", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index c28e3f94..8b3a5184 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -407,6 +407,10 @@ "unlockRotation": "Unlock rotation", "timerActive": "Timer Active", "playbackWillPauseIn": "Playback will pause in ${duration}", + "sleepTimerEndOfVideo": "End of current video", + "sleepTimerStopAtHeader": "Stop at", + "sleepTimerDurationHeader": "Timer", + "playbackWillPauseAtEnd": "Playback will pause at the end of this video", "stillWatching": "Still watching?", "pausingIn": "Pausing in ${seconds}s", "continueWatching": "Continue", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index 18b1fac4..8813e7a5 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -407,6 +407,10 @@ "unlockRotation": "Desbloquear rotación", "timerActive": "Temporizador Activo", "playbackWillPauseIn": "La reproducción se pausará en ${duration}", + "sleepTimerEndOfVideo": "Fin del video actual", + "sleepTimerStopAtHeader": "Detener en", + "sleepTimerDurationHeader": "Temporizador", + "playbackWillPauseAtEnd": "La reproducción se pausará al final de este video", "stillWatching": "¿Sigues viendo?", "pausingIn": "Pausa en ${seconds}s", "continueWatching": "Continuar", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index 910bbbc6..7275db7f 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -407,6 +407,10 @@ "unlockRotation": "Déverrouiller la rotation", "timerActive": "Minuterie active", "playbackWillPauseIn": "La lecture sera mise en pause dans ${duration}", + "sleepTimerEndOfVideo": "Fin de la vidéo actuelle", + "sleepTimerStopAtHeader": "Arrêter à", + "sleepTimerDurationHeader": "Minuterie", + "playbackWillPauseAtEnd": "La lecture sera mise en pause à la fin de cette vidéo", "stillWatching": "Toujours en train de regarder ?", "pausingIn": "Pause dans ${seconds}s", "continueWatching": "Continuer", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index d1bccee9..25bb646a 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -407,6 +407,10 @@ "unlockRotation": "Sblocca rotazione", "timerActive": "Timer attivo", "playbackWillPauseIn": "La riproduzione si interromperà tra ${duration}", + "sleepTimerEndOfVideo": "Fine del video corrente", + "sleepTimerStopAtHeader": "Interrompi a", + "sleepTimerDurationHeader": "Timer", + "playbackWillPauseAtEnd": "La riproduzione si interromperà alla fine di questo video", "stillWatching": "Stai ancora guardando?", "pausingIn": "Pausa tra ${seconds}s", "continueWatching": "Continua", diff --git a/lib/i18n/ja.i18n.json b/lib/i18n/ja.i18n.json index b3ae80e5..3ee40014 100644 --- a/lib/i18n/ja.i18n.json +++ b/lib/i18n/ja.i18n.json @@ -407,6 +407,10 @@ "unlockRotation": "回転のロックを解除", "timerActive": "タイマー動作中", "playbackWillPauseIn": "再生は${duration}後に一時停止します", + "sleepTimerEndOfVideo": "現在の動画の最後", + "sleepTimerStopAtHeader": "停止のタイミング", + "sleepTimerDurationHeader": "タイマー", + "playbackWillPauseAtEnd": "再生はこの動画の最後に一時停止します", "stillWatching": "まだ視聴中ですか?", "pausingIn": "${seconds}秒後に一時停止", "continueWatching": "続ける", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index c0b47423..12505ed7 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -407,6 +407,10 @@ "unlockRotation": "회전 잠금 해제", "timerActive": "타이머 활성화됨", "playbackWillPauseIn": "재생이 ${duration} 후에 일시 중지 됩니다", + "sleepTimerEndOfVideo": "현재 비디오의 끝", + "sleepTimerStopAtHeader": "정지 시점", + "sleepTimerDurationHeader": "타이머", + "playbackWillPauseAtEnd": "재생이 이 비디오의 끝에서 일시 중지됩니다", "stillWatching": "아직 시청 중이신가요?", "pausingIn": "${seconds}초 후 일시 정지", "continueWatching": "계속", diff --git a/lib/i18n/nb.i18n.json b/lib/i18n/nb.i18n.json index 5ca9c0f4..10b25d8f 100644 --- a/lib/i18n/nb.i18n.json +++ b/lib/i18n/nb.i18n.json @@ -407,6 +407,10 @@ "unlockRotation": "Lås opp rotasjon", "timerActive": "Timer aktiv", "playbackWillPauseIn": "Avspilling vil pause om ${duration}", + "sleepTimerEndOfVideo": "Slutten av gjeldende video", + "sleepTimerStopAtHeader": "Stopp ved", + "sleepTimerDurationHeader": "Timer", + "playbackWillPauseAtEnd": "Avspilling vil pause på slutten av denne videoen", "stillWatching": "Ser du fortsatt?", "pausingIn": "Pauser om ${seconds}s", "continueWatching": "Fortsett", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index 39bf98dc..bac132a4 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -407,6 +407,10 @@ "unlockRotation": "Ontgrendel rotatie", "timerActive": "Timer actief", "playbackWillPauseIn": "Afspelen wordt gepauzeerd over ${duration}", + "sleepTimerEndOfVideo": "Einde van huidige video", + "sleepTimerStopAtHeader": "Stoppen bij", + "sleepTimerDurationHeader": "Timer", + "playbackWillPauseAtEnd": "Afspelen wordt gepauzeerd aan het einde van deze video", "stillWatching": "Kijk je nog?", "pausingIn": "Pauze over ${seconds}s", "continueWatching": "Doorgaan", diff --git a/lib/i18n/pl.i18n.json b/lib/i18n/pl.i18n.json index 2424d605..0890f5dd 100644 --- a/lib/i18n/pl.i18n.json +++ b/lib/i18n/pl.i18n.json @@ -407,6 +407,10 @@ "unlockRotation": "Odblokuj obrót", "timerActive": "Wyłącznik aktywny", "playbackWillPauseIn": "Odtwarzanie zatrzyma się za ${duration}", + "sleepTimerEndOfVideo": "Koniec bieżącego wideo", + "sleepTimerStopAtHeader": "Zatrzymaj na", + "sleepTimerDurationHeader": "Minutnik", + "playbackWillPauseAtEnd": "Odtwarzanie zatrzyma się na końcu tego wideo", "stillWatching": "Nadal oglądasz?", "pausingIn": "Pauza za ${seconds}s", "continueWatching": "Kontynuuj", diff --git a/lib/i18n/pt.i18n.json b/lib/i18n/pt.i18n.json index 103e11a8..a6aab8b4 100644 --- a/lib/i18n/pt.i18n.json +++ b/lib/i18n/pt.i18n.json @@ -407,6 +407,10 @@ "unlockRotation": "Destravar rotação", "timerActive": "Timer Ativo", "playbackWillPauseIn": "A reprodução pausará em ${duration}", + "sleepTimerEndOfVideo": "Fim do vídeo atual", + "sleepTimerStopAtHeader": "Parar em", + "sleepTimerDurationHeader": "Timer", + "playbackWillPauseAtEnd": "A reprodução pausará no final deste vídeo", "stillWatching": "Ainda assistindo?", "pausingIn": "Pausando em ${seconds}s", "continueWatching": "Continuar", diff --git a/lib/i18n/ru.i18n.json b/lib/i18n/ru.i18n.json index a22a89ab..60df86c9 100644 --- a/lib/i18n/ru.i18n.json +++ b/lib/i18n/ru.i18n.json @@ -407,6 +407,10 @@ "unlockRotation": "Разблокировать поворот", "timerActive": "Таймер активен", "playbackWillPauseIn": "Воспроизведение будет приостановлено через ${duration}", + "sleepTimerEndOfVideo": "Конец текущего видео", + "sleepTimerStopAtHeader": "Остановить на", + "sleepTimerDurationHeader": "Таймер", + "playbackWillPauseAtEnd": "Воспроизведение будет приостановлено в конце этого видео", "stillWatching": "Всё ещё смотрите?", "pausingIn": "Пауза через ${seconds}с", "continueWatching": "Продолжить", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 8bdeddc3..e9cfc1b9 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 16 -/// Strings: 18976 (1186 per locale) +/// Strings: 19040 (1190 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_bg.g.dart b/lib/i18n/strings_bg.g.dart index fd6dea8e..53814413 100644 --- a/lib/i18n/strings_bg.g.dart +++ b/lib/i18n/strings_bg.g.dart @@ -565,6 +565,10 @@ class _TranslationsVideoControlsBg extends TranslationsVideoControlsEn { @override String get unlockRotation => 'Отключи ротацията'; @override String get timerActive => 'Таймерът е активен'; @override String playbackWillPauseIn({required Object duration}) => 'Възпроизвеждането ще спре след ${duration}'; + @override String get sleepTimerEndOfVideo => 'Край на текущото видео'; + @override String get sleepTimerStopAtHeader => 'Спиране при'; + @override String get sleepTimerDurationHeader => 'Таймер'; + @override String get playbackWillPauseAtEnd => 'Възпроизвеждането ще спре в края на това видео'; @override String get stillWatching => 'Още ли гледате?'; @override String pausingIn({required Object seconds}) => 'Пауза след ${seconds}сек'; @override String get continueWatching => 'Продължи'; @@ -2194,6 +2198,10 @@ extension on TranslationsBg { 'videoControls.unlockRotation' => 'Отключи ротацията', 'videoControls.timerActive' => 'Таймерът е активен', 'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'Възпроизвеждането ще спре след ${duration}', + 'videoControls.sleepTimerEndOfVideo' => 'Край на текущото видео', + 'videoControls.sleepTimerStopAtHeader' => 'Спиране при', + 'videoControls.sleepTimerDurationHeader' => 'Таймер', + 'videoControls.playbackWillPauseAtEnd' => 'Възпроизвеждането ще спре в края на това видео', 'videoControls.stillWatching' => 'Още ли гледате?', 'videoControls.pausingIn' => ({required Object seconds}) => 'Пауза след ${seconds}сек', 'videoControls.continueWatching' => 'Продължи', @@ -2323,12 +2331,12 @@ extension on TranslationsBg { 'mpvConfig.presetSaved' => 'Пресетът е запазен', 'mpvConfig.presetLoaded' => 'Пресетът е зареден', 'mpvConfig.presetDeleted' => 'Пресетът е изтрит', + _ => null, + } ?? switch (path) { 'mpvConfig.confirmDeletePreset' => 'Сигурни ли сте, че искате да изтриете този пресет?', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'dialog.confirmAction' => 'Потвърждение на действие', 'profiles.addPlezyProfile' => 'Добави Plezy профил', - _ => null, - } ?? switch (path) { 'profiles.switchingProfile' => 'Смяна на профил…', 'profiles.deleteThisProfileTitle' => 'Да се изтрие ли този профил?', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Премахване на ${displayName}. Връзките не се засягат.', @@ -2837,12 +2845,12 @@ extension on TranslationsBg { 'externalPlayer.off' => 'Изключено', 'externalPlayer.launchFailed' => 'Неуспешно отваряне на външен плеър', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} не е инсталиран', + _ => null, + } ?? switch (path) { 'externalPlayer.playInExternalPlayer' => 'Пусни във външен плеър', 'metadataEdit.editMetadata' => 'Редактирай...', 'metadataEdit.screenTitle' => 'Редактиране на метаданни', 'metadataEdit.basicInfo' => 'Основна информация', - _ => null, - } ?? switch (path) { 'metadataEdit.artwork' => 'Обложка', 'metadataEdit.advancedSettings' => 'Разширени настройки', 'metadataEdit.title' => 'Заглавие', diff --git a/lib/i18n/strings_da.g.dart b/lib/i18n/strings_da.g.dart index c56878ec..7af178a2 100644 --- a/lib/i18n/strings_da.g.dart +++ b/lib/i18n/strings_da.g.dart @@ -565,6 +565,10 @@ class _TranslationsVideoControlsDa extends TranslationsVideoControlsEn { @override String get unlockRotation => 'Lås rotation op'; @override String get timerActive => 'Timer aktiv'; @override String playbackWillPauseIn({required Object duration}) => 'Afspilning pauses om ${duration}'; + @override String get sleepTimerEndOfVideo => 'Slutningen af aktuel video'; + @override String get sleepTimerStopAtHeader => 'Stop ved'; + @override String get sleepTimerDurationHeader => 'Timer'; + @override String get playbackWillPauseAtEnd => 'Afspilning pauses i slutningen af denne video'; @override String get stillWatching => 'Ser du stadig?'; @override String pausingIn({required Object seconds}) => 'Pauser om ${seconds}s'; @override String get continueWatching => 'Fortsæt'; @@ -2194,6 +2198,10 @@ extension on TranslationsDa { 'videoControls.unlockRotation' => 'Lås rotation op', 'videoControls.timerActive' => 'Timer aktiv', 'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'Afspilning pauses om ${duration}', + 'videoControls.sleepTimerEndOfVideo' => 'Slutningen af aktuel video', + 'videoControls.sleepTimerStopAtHeader' => 'Stop ved', + 'videoControls.sleepTimerDurationHeader' => 'Timer', + 'videoControls.playbackWillPauseAtEnd' => 'Afspilning pauses i slutningen af denne video', 'videoControls.stillWatching' => 'Ser du stadig?', 'videoControls.pausingIn' => ({required Object seconds}) => 'Pauser om ${seconds}s', 'videoControls.continueWatching' => 'Fortsæt', @@ -2323,12 +2331,12 @@ extension on TranslationsDa { 'mpvConfig.presetSaved' => 'Forudindstilling gemt', 'mpvConfig.presetLoaded' => 'Forudindstilling indlæst', 'mpvConfig.presetDeleted' => 'Forudindstilling slettet', + _ => null, + } ?? switch (path) { 'mpvConfig.confirmDeletePreset' => 'Er du sikker på, at du vil slette denne forudindstilling?', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'dialog.confirmAction' => 'Bekræft handling', 'profiles.addPlezyProfile' => 'Tilføj Plezy-profil', - _ => null, - } ?? switch (path) { 'profiles.switchingProfile' => 'Skifter profil…', 'profiles.deleteThisProfileTitle' => 'Slet denne profil?', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Fjern ${displayName}. Forbindelser påvirkes ikke.', @@ -2837,12 +2845,12 @@ extension on TranslationsDa { 'externalPlayer.off' => 'Fra', 'externalPlayer.launchFailed' => 'Kunne ikke åbne ekstern afspiller', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} er ikke installeret', + _ => null, + } ?? switch (path) { 'externalPlayer.playInExternalPlayer' => 'Afspil i ekstern afspiller', 'metadataEdit.editMetadata' => 'Redigér...', 'metadataEdit.screenTitle' => 'Redigér metadata', 'metadataEdit.basicInfo' => 'Grundlæggende info', - _ => null, - } ?? switch (path) { 'metadataEdit.artwork' => 'Grafik', 'metadataEdit.advancedSettings' => 'Avancerede indstillinger', 'metadataEdit.title' => 'Titel', diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index f2c7967f..e7ef4bb5 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -565,6 +565,10 @@ class _TranslationsVideoControlsDe extends TranslationsVideoControlsEn { @override String get unlockRotation => 'Rotation entsperren'; @override String get timerActive => 'Schlaftimer aktiv'; @override String playbackWillPauseIn({required Object duration}) => 'Wiedergabe wird in ${duration} pausiert'; + @override String get sleepTimerEndOfVideo => 'Ende des aktuellen Videos'; + @override String get sleepTimerStopAtHeader => 'Beenden bei'; + @override String get sleepTimerDurationHeader => 'Timer'; + @override String get playbackWillPauseAtEnd => 'Wiedergabe wird am Ende dieses Videos pausiert'; @override String get stillWatching => 'Schaust du noch?'; @override String pausingIn({required Object seconds}) => 'Pause in ${seconds}s'; @override String get continueWatching => 'Weiter'; @@ -2194,6 +2198,10 @@ extension on TranslationsDe { 'videoControls.unlockRotation' => 'Rotation entsperren', 'videoControls.timerActive' => 'Schlaftimer aktiv', 'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'Wiedergabe wird in ${duration} pausiert', + 'videoControls.sleepTimerEndOfVideo' => 'Ende des aktuellen Videos', + 'videoControls.sleepTimerStopAtHeader' => 'Beenden bei', + 'videoControls.sleepTimerDurationHeader' => 'Timer', + 'videoControls.playbackWillPauseAtEnd' => 'Wiedergabe wird am Ende dieses Videos pausiert', 'videoControls.stillWatching' => 'Schaust du noch?', 'videoControls.pausingIn' => ({required Object seconds}) => 'Pause in ${seconds}s', 'videoControls.continueWatching' => 'Weiter', @@ -2323,12 +2331,12 @@ extension on TranslationsDe { 'mpvConfig.presetSaved' => 'Voreinstellung gespeichert', 'mpvConfig.presetLoaded' => 'Voreinstellung geladen', 'mpvConfig.presetDeleted' => 'Voreinstellung gelöscht', + _ => null, + } ?? switch (path) { 'mpvConfig.confirmDeletePreset' => 'Möchten Sie diese Voreinstellung wirklich löschen?', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'dialog.confirmAction' => 'Aktion bestätigen', 'profiles.addPlezyProfile' => 'Plezy-Profil hinzufügen', - _ => null, - } ?? switch (path) { 'profiles.switchingProfile' => 'Profil wird gewechselt…', 'profiles.deleteThisProfileTitle' => 'Dieses Profil löschen?', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => '${displayName} entfernen. Verbindungen bleiben unberührt.', @@ -2837,12 +2845,12 @@ extension on TranslationsDe { 'externalPlayer.off' => 'Aus', 'externalPlayer.launchFailed' => 'Externer Player konnte nicht geöffnet werden', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} ist nicht installiert', + _ => null, + } ?? switch (path) { 'externalPlayer.playInExternalPlayer' => 'In externem Player abspielen', 'metadataEdit.editMetadata' => 'Bearbeiten...', 'metadataEdit.screenTitle' => 'Metadaten bearbeiten', 'metadataEdit.basicInfo' => 'Grundlegende Infos', - _ => null, - } ?? switch (path) { 'metadataEdit.artwork' => 'Artwork', 'metadataEdit.advancedSettings' => 'Erweiterte Einstellungen', 'metadataEdit.title' => 'Titel', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index d5805d47..a9c5a277 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -1276,6 +1276,18 @@ class TranslationsVideoControlsEn { /// en: 'Playback will pause in ${duration}' String playbackWillPauseIn({required Object duration}) => 'Playback will pause in ${duration}'; + /// en: 'End of current video' + String get sleepTimerEndOfVideo => 'End of current video'; + + /// en: 'Stop at' + String get sleepTimerStopAtHeader => 'Stop at'; + + /// en: 'Timer' + String get sleepTimerDurationHeader => 'Timer'; + + /// en: 'Playback will pause at the end of this video' + String get playbackWillPauseAtEnd => 'Playback will pause at the end of this video'; + /// en: 'Still watching?' String get stillWatching => 'Still watching?'; @@ -4572,6 +4584,10 @@ extension on Translations { 'videoControls.unlockRotation' => 'Unlock rotation', 'videoControls.timerActive' => 'Timer Active', 'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'Playback will pause in ${duration}', + 'videoControls.sleepTimerEndOfVideo' => 'End of current video', + 'videoControls.sleepTimerStopAtHeader' => 'Stop at', + 'videoControls.sleepTimerDurationHeader' => 'Timer', + 'videoControls.playbackWillPauseAtEnd' => 'Playback will pause at the end of this video', 'videoControls.stillWatching' => 'Still watching?', 'videoControls.pausingIn' => ({required Object seconds}) => 'Pausing in ${seconds}s', 'videoControls.continueWatching' => 'Continue', @@ -4701,12 +4717,12 @@ extension on Translations { 'mpvConfig.presetSaved' => 'Preset saved', 'mpvConfig.presetLoaded' => 'Preset loaded', 'mpvConfig.presetDeleted' => 'Preset deleted', + _ => null, + } ?? switch (path) { 'mpvConfig.confirmDeletePreset' => 'Are you sure you want to delete this preset?', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'dialog.confirmAction' => 'Confirm Action', 'profiles.addPlezyProfile' => 'Add Plezy profile', - _ => null, - } ?? switch (path) { 'profiles.switchingProfile' => 'Switching profile…', 'profiles.deleteThisProfileTitle' => 'Delete this profile?', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Remove ${displayName}. Connections aren\'t affected.', @@ -5215,12 +5231,12 @@ extension on Translations { 'externalPlayer.off' => 'Off', 'externalPlayer.launchFailed' => 'Failed to open external player', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} is not installed', + _ => null, + } ?? switch (path) { 'externalPlayer.playInExternalPlayer' => 'Play in External Player', 'metadataEdit.editMetadata' => 'Edit...', 'metadataEdit.screenTitle' => 'Edit Metadata', 'metadataEdit.basicInfo' => 'Basic Info', - _ => null, - } ?? switch (path) { 'metadataEdit.artwork' => 'Artwork', 'metadataEdit.advancedSettings' => 'Advanced Settings', 'metadataEdit.title' => 'Title', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index a52f2ccf..47bd4cc3 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -565,6 +565,10 @@ class _TranslationsVideoControlsEs extends TranslationsVideoControlsEn { @override String get unlockRotation => 'Desbloquear rotación'; @override String get timerActive => 'Temporizador Activo'; @override String playbackWillPauseIn({required Object duration}) => 'La reproducción se pausará en ${duration}'; + @override String get sleepTimerEndOfVideo => 'Fin del video actual'; + @override String get sleepTimerStopAtHeader => 'Detener en'; + @override String get sleepTimerDurationHeader => 'Temporizador'; + @override String get playbackWillPauseAtEnd => 'La reproducción se pausará al final de este video'; @override String get stillWatching => '¿Sigues viendo?'; @override String pausingIn({required Object seconds}) => 'Pausa en ${seconds}s'; @override String get continueWatching => 'Continuar'; @@ -2194,6 +2198,10 @@ extension on TranslationsEs { 'videoControls.unlockRotation' => 'Desbloquear rotación', 'videoControls.timerActive' => 'Temporizador Activo', 'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'La reproducción se pausará en ${duration}', + 'videoControls.sleepTimerEndOfVideo' => 'Fin del video actual', + 'videoControls.sleepTimerStopAtHeader' => 'Detener en', + 'videoControls.sleepTimerDurationHeader' => 'Temporizador', + 'videoControls.playbackWillPauseAtEnd' => 'La reproducción se pausará al final de este video', 'videoControls.stillWatching' => '¿Sigues viendo?', 'videoControls.pausingIn' => ({required Object seconds}) => 'Pausa en ${seconds}s', 'videoControls.continueWatching' => 'Continuar', @@ -2323,12 +2331,12 @@ extension on TranslationsEs { 'mpvConfig.presetSaved' => 'Ajuste guardado', 'mpvConfig.presetLoaded' => 'Ajuste cargado', 'mpvConfig.presetDeleted' => 'Ajuste eliminado', + _ => null, + } ?? switch (path) { 'mpvConfig.confirmDeletePreset' => '¿Estás seguro de que quieres eliminar este ajuste?', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'dialog.confirmAction' => 'Confirmar Acción', 'profiles.addPlezyProfile' => 'Añadir perfil de Plezy', - _ => null, - } ?? switch (path) { 'profiles.switchingProfile' => 'Cambiando de perfil…', 'profiles.deleteThisProfileTitle' => '¿Eliminar este perfil?', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Eliminar ${displayName}. Las conexiones no se verán afectadas.', @@ -2837,12 +2845,12 @@ extension on TranslationsEs { 'externalPlayer.off' => 'Desactivado', 'externalPlayer.launchFailed' => 'No se pudo abrir el reproductor externo', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} no está instalado', + _ => null, + } ?? switch (path) { 'externalPlayer.playInExternalPlayer' => 'Reproducir en reproductor externo', 'metadataEdit.editMetadata' => 'Editar...', 'metadataEdit.screenTitle' => 'Editar metadatos', 'metadataEdit.basicInfo' => 'Información básica', - _ => null, - } ?? switch (path) { 'metadataEdit.artwork' => 'Artwork', 'metadataEdit.advancedSettings' => 'Ajustes avanzados', 'metadataEdit.title' => 'Título', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index 91b32219..a2539edd 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -565,6 +565,10 @@ class _TranslationsVideoControlsFr extends TranslationsVideoControlsEn { @override String get unlockRotation => 'Déverrouiller la rotation'; @override String get timerActive => 'Minuterie active'; @override String playbackWillPauseIn({required Object duration}) => 'La lecture sera mise en pause dans ${duration}'; + @override String get sleepTimerEndOfVideo => 'Fin de la vidéo actuelle'; + @override String get sleepTimerStopAtHeader => 'Arrêter à'; + @override String get sleepTimerDurationHeader => 'Minuterie'; + @override String get playbackWillPauseAtEnd => 'La lecture sera mise en pause à la fin de cette vidéo'; @override String get stillWatching => 'Toujours en train de regarder ?'; @override String pausingIn({required Object seconds}) => 'Pause dans ${seconds}s'; @override String get continueWatching => 'Continuer'; @@ -2194,6 +2198,10 @@ extension on TranslationsFr { 'videoControls.unlockRotation' => 'Déverrouiller la rotation', 'videoControls.timerActive' => 'Minuterie active', 'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'La lecture sera mise en pause dans ${duration}', + 'videoControls.sleepTimerEndOfVideo' => 'Fin de la vidéo actuelle', + 'videoControls.sleepTimerStopAtHeader' => 'Arrêter à', + 'videoControls.sleepTimerDurationHeader' => 'Minuterie', + 'videoControls.playbackWillPauseAtEnd' => 'La lecture sera mise en pause à la fin de cette vidéo', 'videoControls.stillWatching' => 'Toujours en train de regarder ?', 'videoControls.pausingIn' => ({required Object seconds}) => 'Pause dans ${seconds}s', 'videoControls.continueWatching' => 'Continuer', @@ -2323,12 +2331,12 @@ extension on TranslationsFr { 'mpvConfig.presetSaved' => 'Préréglage enregistré', 'mpvConfig.presetLoaded' => 'Préréglage chargé', 'mpvConfig.presetDeleted' => 'Préréglage supprimé', + _ => null, + } ?? switch (path) { 'mpvConfig.confirmDeletePreset' => 'Êtes-vous sûr de vouloir supprimer ce préréglage ?', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'dialog.confirmAction' => 'Confirmer l\'action', 'profiles.addPlezyProfile' => 'Ajouter un profil Plezy', - _ => null, - } ?? switch (path) { 'profiles.switchingProfile' => 'Changement de profil…', 'profiles.deleteThisProfileTitle' => 'Supprimer ce profil ?', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Supprimer ${displayName}. Les connexions ne sont pas affectées.', @@ -2837,12 +2845,12 @@ extension on TranslationsFr { 'externalPlayer.off' => 'Désactivé', 'externalPlayer.launchFailed' => 'Impossible d\'ouvrir le lecteur externe', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} n\'est pas installé', + _ => null, + } ?? switch (path) { 'externalPlayer.playInExternalPlayer' => 'Lire dans un lecteur externe', 'metadataEdit.editMetadata' => 'Modifier...', 'metadataEdit.screenTitle' => 'Modifier les métadonnées', 'metadataEdit.basicInfo' => 'Informations de base', - _ => null, - } ?? switch (path) { 'metadataEdit.artwork' => 'Artwork', 'metadataEdit.advancedSettings' => 'Paramètres avancés', 'metadataEdit.title' => 'Titre', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index d8f75253..5750eb32 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -565,6 +565,10 @@ class _TranslationsVideoControlsIt extends TranslationsVideoControlsEn { @override String get unlockRotation => 'Sblocca rotazione'; @override String get timerActive => 'Timer attivo'; @override String playbackWillPauseIn({required Object duration}) => 'La riproduzione si interromperà tra ${duration}'; + @override String get sleepTimerEndOfVideo => 'Fine del video corrente'; + @override String get sleepTimerStopAtHeader => 'Interrompi a'; + @override String get sleepTimerDurationHeader => 'Timer'; + @override String get playbackWillPauseAtEnd => 'La riproduzione si interromperà alla fine di questo video'; @override String get stillWatching => 'Stai ancora guardando?'; @override String pausingIn({required Object seconds}) => 'Pausa tra ${seconds}s'; @override String get continueWatching => 'Continua'; @@ -2194,6 +2198,10 @@ extension on TranslationsIt { 'videoControls.unlockRotation' => 'Sblocca rotazione', 'videoControls.timerActive' => 'Timer attivo', 'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'La riproduzione si interromperà tra ${duration}', + 'videoControls.sleepTimerEndOfVideo' => 'Fine del video corrente', + 'videoControls.sleepTimerStopAtHeader' => 'Interrompi a', + 'videoControls.sleepTimerDurationHeader' => 'Timer', + 'videoControls.playbackWillPauseAtEnd' => 'La riproduzione si interromperà alla fine di questo video', 'videoControls.stillWatching' => 'Stai ancora guardando?', 'videoControls.pausingIn' => ({required Object seconds}) => 'Pausa tra ${seconds}s', 'videoControls.continueWatching' => 'Continua', @@ -2323,12 +2331,12 @@ extension on TranslationsIt { 'mpvConfig.presetSaved' => 'Preset salvato', 'mpvConfig.presetLoaded' => 'Preset caricato', 'mpvConfig.presetDeleted' => 'Preset eliminato', + _ => null, + } ?? switch (path) { 'mpvConfig.confirmDeletePreset' => 'Sei sicuro di voler eliminare questo preset?', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'dialog.confirmAction' => 'Conferma azione', 'profiles.addPlezyProfile' => 'Aggiungi profilo Plezy', - _ => null, - } ?? switch (path) { 'profiles.switchingProfile' => 'Cambio profilo…', 'profiles.deleteThisProfileTitle' => 'Eliminare questo profilo?', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Rimuovi ${displayName}. Le connessioni non sono interessate.', @@ -2837,12 +2845,12 @@ extension on TranslationsIt { 'externalPlayer.off' => 'Disattivato', 'externalPlayer.launchFailed' => 'Impossibile aprire il lettore esterno', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} non è installato', + _ => null, + } ?? switch (path) { 'externalPlayer.playInExternalPlayer' => 'Riproduci in lettore esterno', 'metadataEdit.editMetadata' => 'Modifica...', 'metadataEdit.screenTitle' => 'Modifica metadati', 'metadataEdit.basicInfo' => 'Informazioni di base', - _ => null, - } ?? switch (path) { 'metadataEdit.artwork' => 'Artwork', 'metadataEdit.advancedSettings' => 'Impostazioni avanzate', 'metadataEdit.title' => 'Titolo', diff --git a/lib/i18n/strings_ja.g.dart b/lib/i18n/strings_ja.g.dart index 1b9a1e24..9c1e3f82 100644 --- a/lib/i18n/strings_ja.g.dart +++ b/lib/i18n/strings_ja.g.dart @@ -565,6 +565,10 @@ class _TranslationsVideoControlsJa extends TranslationsVideoControlsEn { @override String get unlockRotation => '回転のロックを解除'; @override String get timerActive => 'タイマー動作中'; @override String playbackWillPauseIn({required Object duration}) => '再生は${duration}後に一時停止します'; + @override String get sleepTimerEndOfVideo => '現在の動画の最後'; + @override String get sleepTimerStopAtHeader => '停止のタイミング'; + @override String get sleepTimerDurationHeader => 'タイマー'; + @override String get playbackWillPauseAtEnd => '再生はこの動画の最後に一時停止します'; @override String get stillWatching => 'まだ視聴中ですか?'; @override String pausingIn({required Object seconds}) => '${seconds}秒後に一時停止'; @override String get continueWatching => '続ける'; @@ -2194,6 +2198,10 @@ extension on TranslationsJa { 'videoControls.unlockRotation' => '回転のロックを解除', 'videoControls.timerActive' => 'タイマー動作中', 'videoControls.playbackWillPauseIn' => ({required Object duration}) => '再生は${duration}後に一時停止します', + 'videoControls.sleepTimerEndOfVideo' => '現在の動画の最後', + 'videoControls.sleepTimerStopAtHeader' => '停止のタイミング', + 'videoControls.sleepTimerDurationHeader' => 'タイマー', + 'videoControls.playbackWillPauseAtEnd' => '再生はこの動画の最後に一時停止します', 'videoControls.stillWatching' => 'まだ視聴中ですか?', 'videoControls.pausingIn' => ({required Object seconds}) => '${seconds}秒後に一時停止', 'videoControls.continueWatching' => '続ける', @@ -2323,12 +2331,12 @@ extension on TranslationsJa { 'mpvConfig.presetSaved' => 'プリセットを保存しました', 'mpvConfig.presetLoaded' => 'プリセットを読み込みました', 'mpvConfig.presetDeleted' => 'プリセットを削除しました', + _ => null, + } ?? switch (path) { 'mpvConfig.confirmDeletePreset' => 'このプリセットを削除してもよろしいですか?', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'dialog.confirmAction' => '操作の確認', 'profiles.addPlezyProfile' => 'Plezyプロファイルを追加', - _ => null, - } ?? switch (path) { 'profiles.switchingProfile' => 'プロファイルを切り替え中…', 'profiles.deleteThisProfileTitle' => 'このプロファイルを削除しますか?', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => '${displayName}を削除します。接続には影響しません。', @@ -2837,12 +2845,12 @@ extension on TranslationsJa { 'externalPlayer.off' => 'オフ', 'externalPlayer.launchFailed' => '外部プレーヤーの起動に失敗しました', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name}がインストールされていません', + _ => null, + } ?? switch (path) { 'externalPlayer.playInExternalPlayer' => '外部プレーヤーで再生', 'metadataEdit.editMetadata' => '編集...', 'metadataEdit.screenTitle' => 'メタデータを編集', 'metadataEdit.basicInfo' => '基本情報', - _ => null, - } ?? switch (path) { 'metadataEdit.artwork' => 'アートワーク', 'metadataEdit.advancedSettings' => '詳細設定', 'metadataEdit.title' => 'タイトル', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index 172176f6..b5509460 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -565,6 +565,10 @@ class _TranslationsVideoControlsKo extends TranslationsVideoControlsEn { @override String get unlockRotation => '회전 잠금 해제'; @override String get timerActive => '타이머 활성화됨'; @override String playbackWillPauseIn({required Object duration}) => '재생이 ${duration} 후에 일시 중지 됩니다'; + @override String get sleepTimerEndOfVideo => '현재 비디오의 끝'; + @override String get sleepTimerStopAtHeader => '정지 시점'; + @override String get sleepTimerDurationHeader => '타이머'; + @override String get playbackWillPauseAtEnd => '재생이 이 비디오의 끝에서 일시 중지됩니다'; @override String get stillWatching => '아직 시청 중이신가요?'; @override String pausingIn({required Object seconds}) => '${seconds}초 후 일시 정지'; @override String get continueWatching => '계속'; @@ -2194,6 +2198,10 @@ extension on TranslationsKo { 'videoControls.unlockRotation' => '회전 잠금 해제', 'videoControls.timerActive' => '타이머 활성화됨', 'videoControls.playbackWillPauseIn' => ({required Object duration}) => '재생이 ${duration} 후에 일시 중지 됩니다', + 'videoControls.sleepTimerEndOfVideo' => '현재 비디오의 끝', + 'videoControls.sleepTimerStopAtHeader' => '정지 시점', + 'videoControls.sleepTimerDurationHeader' => '타이머', + 'videoControls.playbackWillPauseAtEnd' => '재생이 이 비디오의 끝에서 일시 중지됩니다', 'videoControls.stillWatching' => '아직 시청 중이신가요?', 'videoControls.pausingIn' => ({required Object seconds}) => '${seconds}초 후 일시 정지', 'videoControls.continueWatching' => '계속', @@ -2323,12 +2331,12 @@ extension on TranslationsKo { 'mpvConfig.presetSaved' => '프리셋이 저장 되었습니다', 'mpvConfig.presetLoaded' => '프리셋이 로드 되었습니다', 'mpvConfig.presetDeleted' => '프리셋이 삭제 되었습니다', + _ => null, + } ?? switch (path) { 'mpvConfig.confirmDeletePreset' => '이 프리셋을 삭제 하시겠습니까?', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'dialog.confirmAction' => '확인', 'profiles.addPlezyProfile' => 'Plezy 프로필 추가', - _ => null, - } ?? switch (path) { 'profiles.switchingProfile' => '프로필 전환 중…', 'profiles.deleteThisProfileTitle' => '이 프로필을 삭제하시겠습니까?', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => '${displayName}을(를) 제거합니다. 연결에는 영향이 없습니다.', @@ -2837,12 +2845,12 @@ extension on TranslationsKo { 'externalPlayer.off' => '꺼짐', 'externalPlayer.launchFailed' => '외부 플레이어를 열 수 없습니다', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name}이(가) 설치되어 있지 않습니다', + _ => null, + } ?? switch (path) { 'externalPlayer.playInExternalPlayer' => '외부 플레이어에서 재생', 'metadataEdit.editMetadata' => '편집...', 'metadataEdit.screenTitle' => '메타데이터 편집', 'metadataEdit.basicInfo' => '기본 정보', - _ => null, - } ?? switch (path) { 'metadataEdit.artwork' => '아트워크', 'metadataEdit.advancedSettings' => '고급 설정', 'metadataEdit.title' => '제목', diff --git a/lib/i18n/strings_nb.g.dart b/lib/i18n/strings_nb.g.dart index 13b38758..c59e2c50 100644 --- a/lib/i18n/strings_nb.g.dart +++ b/lib/i18n/strings_nb.g.dart @@ -565,6 +565,10 @@ class _TranslationsVideoControlsNb extends TranslationsVideoControlsEn { @override String get unlockRotation => 'Lås opp rotasjon'; @override String get timerActive => 'Timer aktiv'; @override String playbackWillPauseIn({required Object duration}) => 'Avspilling vil pause om ${duration}'; + @override String get sleepTimerEndOfVideo => 'Slutten av gjeldende video'; + @override String get sleepTimerStopAtHeader => 'Stopp ved'; + @override String get sleepTimerDurationHeader => 'Timer'; + @override String get playbackWillPauseAtEnd => 'Avspilling vil pause på slutten av denne videoen'; @override String get stillWatching => 'Ser du fortsatt?'; @override String pausingIn({required Object seconds}) => 'Pauser om ${seconds}s'; @override String get continueWatching => 'Fortsett'; @@ -2194,6 +2198,10 @@ extension on TranslationsNb { 'videoControls.unlockRotation' => 'Lås opp rotasjon', 'videoControls.timerActive' => 'Timer aktiv', 'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'Avspilling vil pause om ${duration}', + 'videoControls.sleepTimerEndOfVideo' => 'Slutten av gjeldende video', + 'videoControls.sleepTimerStopAtHeader' => 'Stopp ved', + 'videoControls.sleepTimerDurationHeader' => 'Timer', + 'videoControls.playbackWillPauseAtEnd' => 'Avspilling vil pause på slutten av denne videoen', 'videoControls.stillWatching' => 'Ser du fortsatt?', 'videoControls.pausingIn' => ({required Object seconds}) => 'Pauser om ${seconds}s', 'videoControls.continueWatching' => 'Fortsett', @@ -2323,12 +2331,12 @@ extension on TranslationsNb { 'mpvConfig.presetSaved' => 'Forhåndsinnstilling lagret', 'mpvConfig.presetLoaded' => 'Forhåndsinnstilling lastet inn', 'mpvConfig.presetDeleted' => 'Forhåndsinnstilling slettet', + _ => null, + } ?? switch (path) { 'mpvConfig.confirmDeletePreset' => 'Er du sikker på at du vil slette denne forhåndsinnstillingen?', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# kommentar', 'dialog.confirmAction' => 'Bekreft handling', 'profiles.addPlezyProfile' => 'Legg til Plezy-profil', - _ => null, - } ?? switch (path) { 'profiles.switchingProfile' => 'Bytter profil…', 'profiles.deleteThisProfileTitle' => 'Slett denne profilen?', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Fjern ${displayName}. Tilkoblinger påvirkes ikke.', @@ -2837,12 +2845,12 @@ extension on TranslationsNb { 'externalPlayer.off' => 'Av', 'externalPlayer.launchFailed' => 'Kunne ikke åpne ekstern spiller', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} er ikke installert', + _ => null, + } ?? switch (path) { 'externalPlayer.playInExternalPlayer' => 'Spill av i ekstern spiller', 'metadataEdit.editMetadata' => 'Rediger...', 'metadataEdit.screenTitle' => 'Rediger metadata', 'metadataEdit.basicInfo' => 'Grunnleggende info', - _ => null, - } ?? switch (path) { 'metadataEdit.artwork' => 'Kunstverk', 'metadataEdit.advancedSettings' => 'Avanserte innstillinger', 'metadataEdit.title' => 'Tittel', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index f5261101..f29c5562 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -565,6 +565,10 @@ class _TranslationsVideoControlsNl extends TranslationsVideoControlsEn { @override String get unlockRotation => 'Ontgrendel rotatie'; @override String get timerActive => 'Timer actief'; @override String playbackWillPauseIn({required Object duration}) => 'Afspelen wordt gepauzeerd over ${duration}'; + @override String get sleepTimerEndOfVideo => 'Einde van huidige video'; + @override String get sleepTimerStopAtHeader => 'Stoppen bij'; + @override String get sleepTimerDurationHeader => 'Timer'; + @override String get playbackWillPauseAtEnd => 'Afspelen wordt gepauzeerd aan het einde van deze video'; @override String get stillWatching => 'Kijk je nog?'; @override String pausingIn({required Object seconds}) => 'Pauze over ${seconds}s'; @override String get continueWatching => 'Doorgaan'; @@ -2194,6 +2198,10 @@ extension on TranslationsNl { 'videoControls.unlockRotation' => 'Ontgrendel rotatie', 'videoControls.timerActive' => 'Timer actief', 'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'Afspelen wordt gepauzeerd over ${duration}', + 'videoControls.sleepTimerEndOfVideo' => 'Einde van huidige video', + 'videoControls.sleepTimerStopAtHeader' => 'Stoppen bij', + 'videoControls.sleepTimerDurationHeader' => 'Timer', + 'videoControls.playbackWillPauseAtEnd' => 'Afspelen wordt gepauzeerd aan het einde van deze video', 'videoControls.stillWatching' => 'Kijk je nog?', 'videoControls.pausingIn' => ({required Object seconds}) => 'Pauze over ${seconds}s', 'videoControls.continueWatching' => 'Doorgaan', @@ -2323,12 +2331,12 @@ extension on TranslationsNl { 'mpvConfig.presetSaved' => 'Voorinstelling opgeslagen', 'mpvConfig.presetLoaded' => 'Voorinstelling geladen', 'mpvConfig.presetDeleted' => 'Voorinstelling verwijderd', + _ => null, + } ?? switch (path) { 'mpvConfig.confirmDeletePreset' => 'Weet je zeker dat je deze voorinstelling wilt verwijderen?', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'dialog.confirmAction' => 'Bevestig actie', 'profiles.addPlezyProfile' => 'Plezy-profiel toevoegen', - _ => null, - } ?? switch (path) { 'profiles.switchingProfile' => 'Profiel wisselen…', 'profiles.deleteThisProfileTitle' => 'Dit profiel verwijderen?', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Verwijder ${displayName}. Verbindingen blijven ongewijzigd.', @@ -2837,12 +2845,12 @@ extension on TranslationsNl { 'externalPlayer.off' => 'Uit', 'externalPlayer.launchFailed' => 'Kan externe speler niet openen', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} is niet geïnstalleerd', + _ => null, + } ?? switch (path) { 'externalPlayer.playInExternalPlayer' => 'Afspelen in externe speler', 'metadataEdit.editMetadata' => 'Bewerken...', 'metadataEdit.screenTitle' => 'Metadata bewerken', 'metadataEdit.basicInfo' => 'Basisinformatie', - _ => null, - } ?? switch (path) { 'metadataEdit.artwork' => 'Artwork', 'metadataEdit.advancedSettings' => 'Geavanceerde instellingen', 'metadataEdit.title' => 'Titel', diff --git a/lib/i18n/strings_pl.g.dart b/lib/i18n/strings_pl.g.dart index 1c4f4e6c..ccfe4cf6 100644 --- a/lib/i18n/strings_pl.g.dart +++ b/lib/i18n/strings_pl.g.dart @@ -565,6 +565,10 @@ class _TranslationsVideoControlsPl extends TranslationsVideoControlsEn { @override String get unlockRotation => 'Odblokuj obrót'; @override String get timerActive => 'Wyłącznik aktywny'; @override String playbackWillPauseIn({required Object duration}) => 'Odtwarzanie zatrzyma się za ${duration}'; + @override String get sleepTimerEndOfVideo => 'Koniec bieżącego wideo'; + @override String get sleepTimerStopAtHeader => 'Zatrzymaj na'; + @override String get sleepTimerDurationHeader => 'Minutnik'; + @override String get playbackWillPauseAtEnd => 'Odtwarzanie zatrzyma się na końcu tego wideo'; @override String get stillWatching => 'Nadal oglądasz?'; @override String pausingIn({required Object seconds}) => 'Pauza za ${seconds}s'; @override String get continueWatching => 'Kontynuuj'; @@ -2194,6 +2198,10 @@ extension on TranslationsPl { 'videoControls.unlockRotation' => 'Odblokuj obrót', 'videoControls.timerActive' => 'Wyłącznik aktywny', 'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'Odtwarzanie zatrzyma się za ${duration}', + 'videoControls.sleepTimerEndOfVideo' => 'Koniec bieżącego wideo', + 'videoControls.sleepTimerStopAtHeader' => 'Zatrzymaj na', + 'videoControls.sleepTimerDurationHeader' => 'Minutnik', + 'videoControls.playbackWillPauseAtEnd' => 'Odtwarzanie zatrzyma się na końcu tego wideo', 'videoControls.stillWatching' => 'Nadal oglądasz?', 'videoControls.pausingIn' => ({required Object seconds}) => 'Pauza za ${seconds}s', 'videoControls.continueWatching' => 'Kontynuuj', @@ -2323,12 +2331,12 @@ extension on TranslationsPl { 'mpvConfig.presetSaved' => 'Preset zapisany', 'mpvConfig.presetLoaded' => 'Preset załadowany', 'mpvConfig.presetDeleted' => 'Preset usunięty', + _ => null, + } ?? switch (path) { 'mpvConfig.confirmDeletePreset' => 'Czy na pewno chcesz usunąć ten preset?', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'dialog.confirmAction' => 'Potwierdź działanie', 'profiles.addPlezyProfile' => 'Dodaj profil Plezy', - _ => null, - } ?? switch (path) { 'profiles.switchingProfile' => 'Przełączanie profilu…', 'profiles.deleteThisProfileTitle' => 'Usunąć ten profil?', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Usuń ${displayName}. Połączenia nie zostaną zmienione.', @@ -2837,12 +2845,12 @@ extension on TranslationsPl { 'externalPlayer.off' => 'Wyłączony', 'externalPlayer.launchFailed' => 'Nie udało się otworzyć zewnętrznego odtwarzacza', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} nie jest zainstalowany', + _ => null, + } ?? switch (path) { 'externalPlayer.playInExternalPlayer' => 'Odtwórz w zewnętrznym odtwarzaczu', 'metadataEdit.editMetadata' => 'Edytuj...', 'metadataEdit.screenTitle' => 'Edytuj metadane', 'metadataEdit.basicInfo' => 'Podstawowe informacje', - _ => null, - } ?? switch (path) { 'metadataEdit.artwork' => 'Grafika', 'metadataEdit.advancedSettings' => 'Ustawienia zaawansowane', 'metadataEdit.title' => 'Tytuł', diff --git a/lib/i18n/strings_pt.g.dart b/lib/i18n/strings_pt.g.dart index 8680834d..ef928c52 100644 --- a/lib/i18n/strings_pt.g.dart +++ b/lib/i18n/strings_pt.g.dart @@ -565,6 +565,10 @@ class _TranslationsVideoControlsPt extends TranslationsVideoControlsEn { @override String get unlockRotation => 'Destravar rotação'; @override String get timerActive => 'Timer Ativo'; @override String playbackWillPauseIn({required Object duration}) => 'A reprodução pausará em ${duration}'; + @override String get sleepTimerEndOfVideo => 'Fim do vídeo atual'; + @override String get sleepTimerStopAtHeader => 'Parar em'; + @override String get sleepTimerDurationHeader => 'Timer'; + @override String get playbackWillPauseAtEnd => 'A reprodução pausará no final deste vídeo'; @override String get stillWatching => 'Ainda assistindo?'; @override String pausingIn({required Object seconds}) => 'Pausando em ${seconds}s'; @override String get continueWatching => 'Continuar'; @@ -2194,6 +2198,10 @@ extension on TranslationsPt { 'videoControls.unlockRotation' => 'Destravar rotação', 'videoControls.timerActive' => 'Timer Ativo', 'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'A reprodução pausará em ${duration}', + 'videoControls.sleepTimerEndOfVideo' => 'Fim do vídeo atual', + 'videoControls.sleepTimerStopAtHeader' => 'Parar em', + 'videoControls.sleepTimerDurationHeader' => 'Timer', + 'videoControls.playbackWillPauseAtEnd' => 'A reprodução pausará no final deste vídeo', 'videoControls.stillWatching' => 'Ainda assistindo?', 'videoControls.pausingIn' => ({required Object seconds}) => 'Pausando em ${seconds}s', 'videoControls.continueWatching' => 'Continuar', @@ -2323,12 +2331,12 @@ extension on TranslationsPt { 'mpvConfig.presetSaved' => 'Predefinição salva', 'mpvConfig.presetLoaded' => 'Predefinição carregada', 'mpvConfig.presetDeleted' => 'Predefinição excluída', + _ => null, + } ?? switch (path) { 'mpvConfig.confirmDeletePreset' => 'Tem certeza que deseja excluir esta predefinição?', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'dialog.confirmAction' => 'Confirmar Ação', 'profiles.addPlezyProfile' => 'Adicionar perfil Plezy', - _ => null, - } ?? switch (path) { 'profiles.switchingProfile' => 'Mudando perfil…', 'profiles.deleteThisProfileTitle' => 'Excluir este perfil?', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Remover ${displayName}. As conexões não serão afetadas.', @@ -2837,12 +2845,12 @@ extension on TranslationsPt { 'externalPlayer.off' => 'Desativado', 'externalPlayer.launchFailed' => 'Falha ao abrir player externo', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} não está instalado', + _ => null, + } ?? switch (path) { 'externalPlayer.playInExternalPlayer' => 'Reproduzir no Player Externo', 'metadataEdit.editMetadata' => 'Editar...', 'metadataEdit.screenTitle' => 'Editar Metadados', 'metadataEdit.basicInfo' => 'Informações Básicas', - _ => null, - } ?? switch (path) { 'metadataEdit.artwork' => 'Arte', 'metadataEdit.advancedSettings' => 'Configurações Avançadas', 'metadataEdit.title' => 'Título', diff --git a/lib/i18n/strings_ru.g.dart b/lib/i18n/strings_ru.g.dart index 7035df38..84ca1447 100644 --- a/lib/i18n/strings_ru.g.dart +++ b/lib/i18n/strings_ru.g.dart @@ -565,6 +565,10 @@ class _TranslationsVideoControlsRu extends TranslationsVideoControlsEn { @override String get unlockRotation => 'Разблокировать поворот'; @override String get timerActive => 'Таймер активен'; @override String playbackWillPauseIn({required Object duration}) => 'Воспроизведение будет приостановлено через ${duration}'; + @override String get sleepTimerEndOfVideo => 'Конец текущего видео'; + @override String get sleepTimerStopAtHeader => 'Остановить на'; + @override String get sleepTimerDurationHeader => 'Таймер'; + @override String get playbackWillPauseAtEnd => 'Воспроизведение будет приостановлено в конце этого видео'; @override String get stillWatching => 'Всё ещё смотрите?'; @override String pausingIn({required Object seconds}) => 'Пауза через ${seconds}с'; @override String get continueWatching => 'Продолжить'; @@ -2194,6 +2198,10 @@ extension on TranslationsRu { 'videoControls.unlockRotation' => 'Разблокировать поворот', 'videoControls.timerActive' => 'Таймер активен', 'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'Воспроизведение будет приостановлено через ${duration}', + 'videoControls.sleepTimerEndOfVideo' => 'Конец текущего видео', + 'videoControls.sleepTimerStopAtHeader' => 'Остановить на', + 'videoControls.sleepTimerDurationHeader' => 'Таймер', + 'videoControls.playbackWillPauseAtEnd' => 'Воспроизведение будет приостановлено в конце этого видео', 'videoControls.stillWatching' => 'Всё ещё смотрите?', 'videoControls.pausingIn' => ({required Object seconds}) => 'Пауза через ${seconds}с', 'videoControls.continueWatching' => 'Продолжить', @@ -2323,12 +2331,12 @@ extension on TranslationsRu { 'mpvConfig.presetSaved' => 'Пресет сохранён', 'mpvConfig.presetLoaded' => 'Пресет загружен', 'mpvConfig.presetDeleted' => 'Пресет удалён', + _ => null, + } ?? switch (path) { 'mpvConfig.confirmDeletePreset' => 'Вы уверены, что хотите удалить этот пресет?', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'dialog.confirmAction' => 'Подтвердить действие', 'profiles.addPlezyProfile' => 'Добавить профиль Plezy', - _ => null, - } ?? switch (path) { 'profiles.switchingProfile' => 'Переключение профиля…', 'profiles.deleteThisProfileTitle' => 'Удалить этот профиль?', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Удалить ${displayName}. Подключения не изменятся.', @@ -2837,12 +2845,12 @@ extension on TranslationsRu { 'externalPlayer.off' => 'Выкл.', 'externalPlayer.launchFailed' => 'Не удалось открыть внешний плеер', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} не установлен', + _ => null, + } ?? switch (path) { 'externalPlayer.playInExternalPlayer' => 'Воспроизвести во внешнем плеере', 'metadataEdit.editMetadata' => 'Редактировать...', 'metadataEdit.screenTitle' => 'Редактировать метаданные', 'metadataEdit.basicInfo' => 'Основная информация', - _ => null, - } ?? switch (path) { 'metadataEdit.artwork' => 'Обложка', 'metadataEdit.advancedSettings' => 'Дополнительные настройки', 'metadataEdit.title' => 'Название', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index efe9e085..eaac3ee3 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -565,6 +565,10 @@ class _TranslationsVideoControlsSv extends TranslationsVideoControlsEn { @override String get unlockRotation => 'Lås upp rotation'; @override String get timerActive => 'Timer aktiv'; @override String playbackWillPauseIn({required Object duration}) => 'Uppspelningen pausas om ${duration}'; + @override String get sleepTimerEndOfVideo => 'Slutet av aktuell video'; + @override String get sleepTimerStopAtHeader => 'Stoppa vid'; + @override String get sleepTimerDurationHeader => 'Timer'; + @override String get playbackWillPauseAtEnd => 'Uppspelningen pausas i slutet av denna video'; @override String get stillWatching => 'Tittar du fortfarande?'; @override String pausingIn({required Object seconds}) => 'Pausar om ${seconds}s'; @override String get continueWatching => 'Fortsätt'; @@ -2194,6 +2198,10 @@ extension on TranslationsSv { 'videoControls.unlockRotation' => 'Lås upp rotation', 'videoControls.timerActive' => 'Timer aktiv', 'videoControls.playbackWillPauseIn' => ({required Object duration}) => 'Uppspelningen pausas om ${duration}', + 'videoControls.sleepTimerEndOfVideo' => 'Slutet av aktuell video', + 'videoControls.sleepTimerStopAtHeader' => 'Stoppa vid', + 'videoControls.sleepTimerDurationHeader' => 'Timer', + 'videoControls.playbackWillPauseAtEnd' => 'Uppspelningen pausas i slutet av denna video', 'videoControls.stillWatching' => 'Tittar du fortfarande?', 'videoControls.pausingIn' => ({required Object seconds}) => 'Pausar om ${seconds}s', 'videoControls.continueWatching' => 'Fortsätt', @@ -2323,12 +2331,12 @@ extension on TranslationsSv { 'mpvConfig.presetSaved' => 'Förval sparat', 'mpvConfig.presetLoaded' => 'Förval laddat', 'mpvConfig.presetDeleted' => 'Förval borttaget', + _ => null, + } ?? switch (path) { 'mpvConfig.confirmDeletePreset' => 'Är du säker på att du vill ta bort detta förval?', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'dialog.confirmAction' => 'Bekräfta åtgärd', 'profiles.addPlezyProfile' => 'Lägg till Plezy-profil', - _ => null, - } ?? switch (path) { 'profiles.switchingProfile' => 'Byter profil…', 'profiles.deleteThisProfileTitle' => 'Ta bort denna profil?', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => 'Ta bort ${displayName}. Anslutningar påverkas inte.', @@ -2837,12 +2845,12 @@ extension on TranslationsSv { 'externalPlayer.off' => 'Av', 'externalPlayer.launchFailed' => 'Kunde inte öppna extern spelare', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} är inte installerad', + _ => null, + } ?? switch (path) { 'externalPlayer.playInExternalPlayer' => 'Spela i extern spelare', 'metadataEdit.editMetadata' => 'Redigera...', 'metadataEdit.screenTitle' => 'Redigera metadata', 'metadataEdit.basicInfo' => 'Grundläggande info', - _ => null, - } ?? switch (path) { 'metadataEdit.artwork' => 'Artwork', 'metadataEdit.advancedSettings' => 'Avancerade inställningar', 'metadataEdit.title' => 'Titel', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index 05369c37..f17726d3 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -565,6 +565,10 @@ class _TranslationsVideoControlsZh extends TranslationsVideoControlsEn { @override String get unlockRotation => '解锁旋转'; @override String get timerActive => '定时器已激活'; @override String playbackWillPauseIn({required Object duration}) => '播放将在 ${duration} 后暂停'; + @override String get sleepTimerEndOfVideo => '当前视频结束时'; + @override String get sleepTimerStopAtHeader => '停止于'; + @override String get sleepTimerDurationHeader => '定时器'; + @override String get playbackWillPauseAtEnd => '播放将在此视频结束时暂停'; @override String get stillWatching => '还在看吗?'; @override String pausingIn({required Object seconds}) => '${seconds}秒后暂停'; @override String get continueWatching => '继续'; @@ -2194,6 +2198,10 @@ extension on TranslationsZh { 'videoControls.unlockRotation' => '解锁旋转', 'videoControls.timerActive' => '定时器已激活', 'videoControls.playbackWillPauseIn' => ({required Object duration}) => '播放将在 ${duration} 后暂停', + 'videoControls.sleepTimerEndOfVideo' => '当前视频结束时', + 'videoControls.sleepTimerStopAtHeader' => '停止于', + 'videoControls.sleepTimerDurationHeader' => '定时器', + 'videoControls.playbackWillPauseAtEnd' => '播放将在此视频结束时暂停', 'videoControls.stillWatching' => '还在看吗?', 'videoControls.pausingIn' => ({required Object seconds}) => '${seconds}秒后暂停', 'videoControls.continueWatching' => '继续', @@ -2323,12 +2331,12 @@ extension on TranslationsZh { 'mpvConfig.presetSaved' => '预设已保存', 'mpvConfig.presetLoaded' => '预设已加载', 'mpvConfig.presetDeleted' => '预设已删除', + _ => null, + } ?? switch (path) { 'mpvConfig.confirmDeletePreset' => '确定要删除此预设吗?', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'dialog.confirmAction' => '确认操作', 'profiles.addPlezyProfile' => '添加 Plezy 配置文件', - _ => null, - } ?? switch (path) { 'profiles.switchingProfile' => '切换配置文件中…', 'profiles.deleteThisProfileTitle' => '删除此配置文件?', 'profiles.deleteThisProfileMessage' => ({required Object displayName}) => '移除 ${displayName}。连接不会受影响。', @@ -2837,12 +2845,12 @@ extension on TranslationsZh { 'externalPlayer.off' => '关闭', 'externalPlayer.launchFailed' => '无法打开外部播放器', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} 未安装', + _ => null, + } ?? switch (path) { 'externalPlayer.playInExternalPlayer' => '在外部播放器中播放', 'metadataEdit.editMetadata' => '编辑...', 'metadataEdit.screenTitle' => '编辑元数据', 'metadataEdit.basicInfo' => '基本信息', - _ => null, - } ?? switch (path) { 'metadataEdit.artwork' => '封面图片', 'metadataEdit.advancedSettings' => '高级设置', 'metadataEdit.title' => '标题', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 6c9a3d27..cc8f6e26 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -407,6 +407,10 @@ "unlockRotation": "Lås upp rotation", "timerActive": "Timer aktiv", "playbackWillPauseIn": "Uppspelningen pausas om ${duration}", + "sleepTimerEndOfVideo": "Slutet av aktuell video", + "sleepTimerStopAtHeader": "Stoppa vid", + "sleepTimerDurationHeader": "Timer", + "playbackWillPauseAtEnd": "Uppspelningen pausas i slutet av denna video", "stillWatching": "Tittar du fortfarande?", "pausingIn": "Pausar om ${seconds}s", "continueWatching": "Fortsätt", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index 01d0fc5a..951ea3f5 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -407,6 +407,10 @@ "unlockRotation": "解锁旋转", "timerActive": "定时器已激活", "playbackWillPauseIn": "播放将在 ${duration} 后暂停", + "sleepTimerEndOfVideo": "当前视频结束时", + "sleepTimerStopAtHeader": "停止于", + "sleepTimerDurationHeader": "定时器", + "playbackWillPauseAtEnd": "播放将在此视频结束时暂停", "stillWatching": "还在看吗?", "pausingIn": "${seconds}秒后暂停", "continueWatching": "继续", diff --git a/lib/screens/video_player/parts/playback_prompts.dart b/lib/screens/video_player/parts/playback_prompts.dart index 6481d1a5..6bea0020 100644 --- a/lib/screens/video_player/parts/playback_prompts.dart +++ b/lib/screens/video_player/parts/playback_prompts.dart @@ -25,6 +25,15 @@ extension _VideoPlayerPlaybackPromptMethods on VideoPlayerScreenState { unawaited(_videoPIPManager?.updateAutoPipState(isPlaying: false)); } + // End-of-video sleep timer takes precedence over autoplay / next-episode + // dialogs: the user explicitly asked to stop after this item. + final sleepTimerService = SleepTimerService(); + if (sleepTimerService.isEndOfVideoMode && !_completionTriggered) { + _completionTriggered = true; + sleepTimerService.notifyVideoCompleted(); + return; + } + if (_nextEpisode != null && !_showPlayNextDialog && !_showStillWatchingPrompt && !_completionTriggered) { _completionTriggered = true; diff --git a/lib/services/sleep_timer_service.dart b/lib/services/sleep_timer_service.dart index 90f88284..7361e718 100644 --- a/lib/services/sleep_timer_service.dart +++ b/lib/services/sleep_timer_service.dart @@ -15,6 +15,8 @@ class SleepTimerService extends ChangeNotifier { Duration? _originalDuration; VoidCallback? _onTimerComplete; bool _needsRestart = false; + bool _restartAsEndOfVideo = false; + bool _endOfVideoArmed = false; final StreamController _completedController = StreamController.broadcast(); final StreamController _promptController = StreamController.broadcast(); @@ -24,7 +26,11 @@ class SleepTimerService extends ChangeNotifier { /// Emits when the timer fires and wants to show a "still watching?" prompt Stream get onPrompt => _promptController.stream; - bool get isActive => _timer != null && _timer!.isActive; + bool get isActive => (_timer != null && _timer!.isActive) || _endOfVideoArmed; + + /// Whether the sleep timer is armed to fire at the end of the current video + /// rather than after a fixed duration. + bool get isEndOfVideoMode => _endOfVideoArmed; /// The time when the timer will complete DateTime? get endTime => _endTime; @@ -67,9 +73,34 @@ class SleepTimerService extends ChangeNotifier { notifyListeners(); } + /// Arm the sleep timer to fire when the currently playing video reaches its end. + /// Unlike [startTimer], no periodic timer runs — playback completion is reported + /// externally via [notifyVideoCompleted]. + void armEndOfVideo(VoidCallback onComplete) { + cancelTimer(); + + _endOfVideoArmed = true; + _onTimerComplete = onComplete; + + appLogger.d('Sleep timer armed: end of current video'); + notifyListeners(); + } + + /// Notify the service that the currently playing video has completed. + /// If armed via [armEndOfVideo], fires the completion callback and resets the mode. + /// Safe to call unconditionally — does nothing when end-of-video mode is not armed. + void notifyVideoCompleted() { + if (!_endOfVideoArmed) return; + + appLogger.d('Sleep timer (end of video) triggered'); + _endOfVideoArmed = false; + _executeCallback(); + notifyListeners(); + } + /// Cancel the active timer (user-initiated, clears everything) void cancelTimer() { - if (_timer != null || _originalDuration != null) { + if (_timer != null || _originalDuration != null || _endOfVideoArmed) { appLogger.d('Sleep timer cancelled'); _timer?.cancel(); _timer = null; @@ -77,6 +108,8 @@ class SleepTimerService extends ChangeNotifier { _duration = null; _originalDuration = null; _onTimerComplete = null; + _endOfVideoArmed = false; + _restartAsEndOfVideo = false; notifyListeners(); } } @@ -99,14 +132,20 @@ class SleepTimerService extends ChangeNotifier { void markNeedsRestart() { if (isActive || _originalDuration != null) { _needsRestart = true; + _restartAsEndOfVideo = _endOfVideoArmed; } } /// 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; + if (!_needsRestart) return; + _needsRestart = false; + + if (_restartAsEndOfVideo) { + _restartAsEndOfVideo = false; + armEndOfVideo(onComplete); + } else if (_originalDuration != null) { startTimer(_originalDuration!, onComplete); } } diff --git a/lib/widgets/video_controls/widgets/sleep_timer_active_status.dart b/lib/widgets/video_controls/widgets/sleep_timer_active_status.dart index eafc0f34..f28b290c 100644 --- a/lib/widgets/video_controls/widgets/sleep_timer_active_status.dart +++ b/lib/widgets/video_controls/widgets/sleep_timer_active_status.dart @@ -6,16 +6,27 @@ import '../../../services/sleep_timer_service.dart'; import '../../../i18n/strings.g.dart'; import '../../../utils/formatters.dart'; -/// Widget displaying active sleep timer status with extend/cancel actions +/// Widget displaying active sleep timer status with extend/cancel actions. +/// +/// Supports two modes: +/// * Timed (default) — [remainingTime] is non-null; shows countdown and the +/// "+15 min" extend button. +/// * End-of-video — [remainingTime] is null; shows a static message and only +/// the cancel button (extending makes no sense here). class SleepTimerActiveStatus extends StatelessWidget { final SleepTimerService sleepTimer; - final Duration remainingTime; + final Duration? remainingTime; final VoidCallback? onCancel; - const SleepTimerActiveStatus({super.key, required this.sleepTimer, required this.remainingTime, this.onCancel}); + const SleepTimerActiveStatus({super.key, required this.sleepTimer, this.remainingTime, this.onCancel}); @override Widget build(BuildContext context) { + final isEndOfVideo = remainingTime == null; + final subtitle = isEndOfVideo + ? t.videoControls.playbackWillPauseAtEnd + : t.videoControls.playbackWillPauseIn(duration: formatDurationWithSeconds(remainingTime!)); + return Container( padding: const EdgeInsets.all(16), color: Colors.amber.withValues(alpha: 0.1), @@ -27,30 +38,33 @@ class SleepTimerActiveStatus extends StatelessWidget { ), const SizedBox(height: 8), Text( - t.videoControls.playbackWillPauseIn(duration: formatDurationWithSeconds(remainingTime)), + subtitle, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 14), + textAlign: TextAlign.center, ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - FocusableButton( - onPressed: () { - sleepTimer.extendTimer(const Duration(minutes: 15)); - }, - child: OutlinedButton.icon( - icon: const AppIcon(Symbols.add_rounded, fill: 1), - label: Text(t.videoControls.addTime(amount: "15", unit: " min")), - style: OutlinedButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.onSurface, - side: BorderSide(color: Theme.of(context).colorScheme.outline), - ), + if (!isEndOfVideo) ...[ + FocusableButton( onPressed: () { sleepTimer.extendTimer(const Duration(minutes: 15)); }, + child: OutlinedButton.icon( + icon: const AppIcon(Symbols.add_rounded, fill: 1), + label: Text(t.videoControls.addTime(amount: "15", unit: " min")), + style: OutlinedButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.onSurface, + side: BorderSide(color: Theme.of(context).colorScheme.outline), + ), + onPressed: () { + sleepTimer.extendTimer(const Duration(minutes: 15)); + }, + ), ), - ), - const SizedBox(width: 12), + const SizedBox(width: 12), + ], FocusableButton( onPressed: () { sleepTimer.cancelTimer(); diff --git a/lib/widgets/video_controls/widgets/sleep_timer_content.dart b/lib/widgets/video_controls/widgets/sleep_timer_content.dart index 65141925..9394c270 100644 --- a/lib/widgets/video_controls/widgets/sleep_timer_content.dart +++ b/lib/widgets/video_controls/widgets/sleep_timer_content.dart @@ -1,11 +1,28 @@ import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import '../../../i18n/strings.g.dart'; import '../../../mpv/mpv.dart'; import '../../../services/sleep_timer_service.dart'; +import '../../../utils/formatters.dart'; +import '../../../utils/snackbar_helper.dart'; +import '../../../widgets/app_icon.dart'; +import '../../../widgets/focusable_list_tile.dart'; +import '../../../widgets/overlay_sheet.dart'; +import '../sheets/sheet_column_header.dart'; import 'sleep_timer_active_status.dart'; -import 'sleep_timer_duration_list.dart'; /// Shared UI for sleep timer selection and active status. +/// +/// Layout mirrors the audio/subtitle [TrackSheet]: two side-by-side columns +/// inside a [Row], each in its own [FocusTraversalGroup] so D-pad navigation +/// stays inside the column the user is acting on. +/// +/// * Left column ("Stop at") — event-based stop options. Today this is just +/// "End of current video"; the column is intentionally open-ended for +/// future event triggers (end of next episode, end of chapter, …). +/// * Right column ("Timer") — fixed-duration options (5/10/15/30/45/60/90/120 +/// minutes, plus an optional default duration injected from settings). class SleepTimerContent extends StatelessWidget { final Player player; final SleepTimerService sleepTimer; @@ -26,15 +43,37 @@ class SleepTimerContent extends StatelessWidget { listenable: sleepTimer, builder: (context, _) { final remainingTime = sleepTimer.remainingTime; + // Active status renders either with a countdown (duration timer) or + // without (end-of-video mode). Both share the same widget. + final showActiveStatus = sleepTimer.isActive && (remainingTime != null || sleepTimer.isEndOfVideoMode); return Column( children: [ - if (sleepTimer.isActive && remainingTime != null) ...[ + if (showActiveStatus) ...[ SleepTimerActiveStatus(sleepTimer: sleepTimer, remainingTime: remainingTime, onCancel: onCancel), Divider(color: Theme.of(context).dividerColor, height: 1), ], Expanded( - child: SleepTimerDurationList(player: player, sleepTimer: sleepTimer, defaultDuration: defaultDuration), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: FocusTraversalGroup( + child: _SleepTimerEventColumn(player: player, sleepTimer: sleepTimer), + ), + ), + VerticalDivider(width: 1, color: Theme.of(context).dividerColor), + Expanded( + child: FocusTraversalGroup( + child: _SleepTimerDurationColumn( + player: player, + sleepTimer: sleepTimer, + defaultDuration: defaultDuration, + ), + ), + ), + ], + ), ), ], ); @@ -42,3 +81,103 @@ class SleepTimerContent extends StatelessWidget { ); } } + +class _SleepTimerEventColumn extends StatelessWidget { + final Player player; + final SleepTimerService sleepTimer; + + const _SleepTimerEventColumn({required this.player, required this.sleepTimer}); + + @override + Widget build(BuildContext context) { + final label = t.videoControls.sleepTimerEndOfVideo; + + return Column( + children: [ + SheetColumnHeader(label: t.videoControls.sleepTimerStopAtHeader), + Expanded( + child: ListView( + children: [ + FocusableListTile( + leading: AppIcon( + Symbols.hourglass_bottom_rounded, + fill: 1, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + title: Text(label), + selected: sleepTimer.isEndOfVideoMode, + onTap: () { + sleepTimer.armEndOfVideo(() { + // Pause playback when the current video ends + player.pause(); + }); + OverlaySheetController.closeAdaptive(context); + + showSuccessSnackBar(context, t.messages.sleepTimerSet(label: label)); + }, + ), + ], + ), + ), + ], + ); + } +} + +class _SleepTimerDurationColumn extends StatelessWidget { + final Player player; + final SleepTimerService sleepTimer; + final int? defaultDuration; + + const _SleepTimerDurationColumn({required this.player, required this.sleepTimer, this.defaultDuration}); + + @override + Widget build(BuildContext context) { + final durations = [5, 10, 15, 30, 45, 60, 90, 120]; + // Add default duration if provided and not already in list + if (defaultDuration != null && !durations.contains(defaultDuration)) { + durations.add(defaultDuration!); + durations.sort(); + } + + // Highlight the currently running duration timer (end-of-video mode is + // handled by the event column and intentionally does not light up here). + final activeMinutes = sleepTimer.isActive && !sleepTimer.isEndOfVideoMode + ? sleepTimer.originalDuration?.inMinutes + : null; + + return Column( + children: [ + SheetColumnHeader(label: t.videoControls.sleepTimerDurationHeader), + Expanded( + child: ListView.builder( + itemCount: durations.length, + itemBuilder: (context, index) { + final minutes = durations[index]; + final label = formatDurationTextual( + minutes * 60 * 1000, // Convert minutes to milliseconds + abbreviated: false, // Use full format for better readability + ); + + return FocusableListTile( + leading: AppIcon(Symbols.timer_rounded, fill: 1, color: Theme.of(context).colorScheme.onSurfaceVariant), + title: Text(label), + selected: minutes == activeMinutes, + onTap: () { + sleepTimer.startTimer(Duration(minutes: minutes), () { + // Pause playback when timer completes + player.pause(); + }); + OverlaySheetController.closeAdaptive(context); + + // Show confirmation snackbar + showSuccessSnackBar(context, t.messages.sleepTimerSet(label: label)); + }, + ); + }, + ), + ), + ], + ); + } +} diff --git a/lib/widgets/video_controls/widgets/sleep_timer_duration_list.dart b/lib/widgets/video_controls/widgets/sleep_timer_duration_list.dart deleted file mode 100644 index 81f8550c..00000000 --- a/lib/widgets/video_controls/widgets/sleep_timer_duration_list.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:plezy/widgets/app_icon.dart'; -import 'package:material_symbols_icons/symbols.dart'; - -import '../../../mpv/mpv.dart'; -import '../../../services/sleep_timer_service.dart'; -import '../../../utils/formatters.dart'; -import '../../../utils/snackbar_helper.dart'; -import '../../../widgets/overlay_sheet.dart'; -import '../../../i18n/strings.g.dart'; - -class SleepTimerDurationList extends StatelessWidget { - final Player player; - final SleepTimerService sleepTimer; - final int? defaultDuration; - - const SleepTimerDurationList({super.key, required this.player, required this.sleepTimer, this.defaultDuration}); - - @override - Widget build(BuildContext context) { - final durations = [5, 10, 15, 30, 45, 60, 90, 120]; - // Add default duration if provided and not already in list - if (defaultDuration != null && !durations.contains(defaultDuration)) { - durations.add(defaultDuration!); - durations.sort(); - } - - return ListView.builder( - itemCount: durations.length, - itemBuilder: (context, index) { - final minutes = durations[index]; - final label = formatDurationTextual( - minutes * 60 * 1000, // Convert minutes to milliseconds - abbreviated: false, // Use full format for better readability - ); - - return ListTile( - leading: AppIcon(Symbols.timer_rounded, fill: 1, color: Theme.of(context).colorScheme.onSurfaceVariant), - title: Text(label), - onTap: () { - sleepTimer.startTimer(Duration(minutes: minutes), () { - // Pause playback when timer completes - player.pause(); - }); - OverlaySheetController.closeAdaptive(context); - - // Show confirmation snackbar - showSuccessSnackBar(context, t.messages.sleepTimerSet(label: label)); - }, - ); - }, - ); - } -} diff --git a/test/services/sleep_timer_service_test.dart b/test/services/sleep_timer_service_test.dart index 1d726941..657a470f 100644 --- a/test/services/sleep_timer_service_test.dart +++ b/test/services/sleep_timer_service_test.dart @@ -287,6 +287,120 @@ void main() { }); }); + // ============================================================ + // armEndOfVideo / notifyVideoCompleted + // ============================================================ + + group('armEndOfVideo', () { + test('sets isActive and isEndOfVideoMode without starting a periodic timer', () { + timer.armEndOfVideo(() {}); + try { + expect(timer.isActive, isTrue); + expect(timer.isEndOfVideoMode, isTrue); + // No fixed duration / endTime in this mode — countdown UIs must + // detect end-of-video mode instead of falling through. + expect(timer.endTime, isNull); + expect(timer.duration, isNull); + expect(timer.originalDuration, isNull); + expect(timer.remainingTime, isNull); + } finally { + timer.cancelTimer(); + } + }); + + test('replaces any running duration-based timer', () { + var firstFired = false; + timer.startTimer(const Duration(minutes: 30), () => firstFired = true); + expect(timer.isEndOfVideoMode, isFalse); + + timer.armEndOfVideo(() {}); + expect(timer.isEndOfVideoMode, isTrue); + // The previous duration is gone — armEndOfVideo calls cancelTimer first. + expect(timer.originalDuration, isNull); + expect(firstFired, isFalse); + + timer.cancelTimer(); + }); + + test('cancelTimer clears end-of-video mode', () { + timer.armEndOfVideo(() {}); + timer.cancelTimer(); + expect(timer.isActive, isFalse); + expect(timer.isEndOfVideoMode, isFalse); + }); + + test('arming notifies listeners', () { + var notifications = 0; + void listener() => notifications++; + timer.addListener(listener); + + timer.armEndOfVideo(() {}); + // cancelTimer (called inside armEndOfVideo when idle) is gated on existing + // state, so a fresh service only emits the single arm notification. + expect(notifications, greaterThanOrEqualTo(1)); + + timer.removeListener(listener); + timer.cancelTimer(); + }); + }); + + group('notifyVideoCompleted', () { + test('fires the stored callback and emits onCompleted', () async { + var fired = 0; + var completedFired = 0; + final sub = timer.onCompleted.listen((_) => completedFired++); + + timer.armEndOfVideo(() => fired++); + timer.notifyVideoCompleted(); + await Future.delayed(Duration.zero); + + expect(fired, 1); + expect(completedFired, 1); + // Mode is consumed after firing. + expect(timer.isEndOfVideoMode, isFalse); + expect(timer.isActive, isFalse); + + await sub.cancel(); + }); + + test('does nothing when end-of-video mode is not armed', () async { + var completedFired = 0; + final sub = timer.onCompleted.listen((_) => completedFired++); + + timer.notifyVideoCompleted(); + await Future.delayed(Duration.zero); + expect(completedFired, 0); + + // Also safe when a duration timer is running — should not interfere. + timer.startTimer(const Duration(minutes: 30), () {}); + timer.notifyVideoCompleted(); + await Future.delayed(Duration.zero); + expect(completedFired, 0); + expect(timer.isActive, isTrue); + + await sub.cancel(); + timer.cancelTimer(); + }); + }); + + group('end-of-video + restartIfNeeded', () { + test('preserves the end-of-video mode across a playback session swap', () { + timer.armEndOfVideo(() {}); + timer.markNeedsRestart(); + + var newCallbackHooked = false; + timer.restartIfNeeded(() => newCallbackHooked = true); + + expect(timer.isActive, isTrue); + expect(timer.isEndOfVideoMode, isTrue); + // restartIfNeeded re-arms — the consumer callback should be wired up + // but not yet invoked. + expect(newCallbackHooked, isFalse); + + timer.cancelTimer(); + }); + }); + // ============================================================ // What's NOT covered (and why) // ============================================================