feat(player): add end-of-video sleep timer option (#1111)

close #540
This commit is contained in:
Timo Kox
2026-05-30 07:16:01 +02:00
committed by GitHub
parent a4f0b92b4e
commit 294996eac1
39 changed files with 604 additions and 143 deletions
+4
View File
@@ -407,6 +407,10 @@
"unlockRotation": "Отключи ротацията",
"timerActive": "Таймерът е активен",
"playbackWillPauseIn": "Възпроизвеждането ще спре след ${duration}",
"sleepTimerEndOfVideo": "Край на текущото видео",
"sleepTimerStopAtHeader": "Спиране при",
"sleepTimerDurationHeader": "Таймер",
"playbackWillPauseAtEnd": "Възпроизвеждането ще спре в края на това видео",
"stillWatching": "Още ли гледате?",
"pausingIn": "Пауза след ${seconds}сек",
"continueWatching": "Продължи",
+4
View File
@@ -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",
+4
View File
@@ -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",
+4
View File
@@ -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",
+4
View File
@@ -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",
+4
View File
@@ -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",
+4
View File
@@ -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",
+4
View File
@@ -407,6 +407,10 @@
"unlockRotation": "回転のロックを解除",
"timerActive": "タイマー動作中",
"playbackWillPauseIn": "再生は${duration}後に一時停止します",
"sleepTimerEndOfVideo": "現在の動画の最後",
"sleepTimerStopAtHeader": "停止のタイミング",
"sleepTimerDurationHeader": "タイマー",
"playbackWillPauseAtEnd": "再生はこの動画の最後に一時停止します",
"stillWatching": "まだ視聴中ですか?",
"pausingIn": "${seconds}秒後に一時停止",
"continueWatching": "続ける",
+4
View File
@@ -407,6 +407,10 @@
"unlockRotation": "회전 잠금 해제",
"timerActive": "타이머 활성화됨",
"playbackWillPauseIn": "재생이 ${duration} 후에 일시 중지 됩니다",
"sleepTimerEndOfVideo": "현재 비디오의 끝",
"sleepTimerStopAtHeader": "정지 시점",
"sleepTimerDurationHeader": "타이머",
"playbackWillPauseAtEnd": "재생이 이 비디오의 끝에서 일시 중지됩니다",
"stillWatching": "아직 시청 중이신가요?",
"pausingIn": "${seconds}초 후 일시 정지",
"continueWatching": "계속",
+4
View File
@@ -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",
+4
View File
@@ -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",
+4
View File
@@ -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",
+4
View File
@@ -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",
+4
View File
@@ -407,6 +407,10 @@
"unlockRotation": "Разблокировать поворот",
"timerActive": "Таймер активен",
"playbackWillPauseIn": "Воспроизведение будет приостановлено через ${duration}",
"sleepTimerEndOfVideo": "Конец текущего видео",
"sleepTimerStopAtHeader": "Остановить на",
"sleepTimerDurationHeader": "Таймер",
"playbackWillPauseAtEnd": "Воспроизведение будет приостановлено в конце этого видео",
"stillWatching": "Всё ещё смотрите?",
"pausingIn": "Пауза через ${seconds}с",
"continueWatching": "Продолжить",
+1 -1
View File
@@ -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
+12 -4
View File
@@ -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' => 'Заглавие',
+12 -4
View File
@@ -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',
+12 -4
View File
@@ -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',
+20 -4
View File
@@ -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',
+12 -4
View File
@@ -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',
+12 -4
View File
@@ -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',
+12 -4
View File
@@ -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',
+12 -4
View File
@@ -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' => 'タイトル',
+12 -4
View File
@@ -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' => '제목',
+12 -4
View File
@@ -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',
+12 -4
View File
@@ -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',
+12 -4
View File
@@ -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ł',
+12 -4
View File
@@ -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',
+12 -4
View File
@@ -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' => 'Название',
+12 -4
View File
@@ -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',
+12 -4
View File
@@ -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' => '标题',
+4
View File
@@ -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",
+4
View File
@@ -407,6 +407,10 @@
"unlockRotation": "解锁旋转",
"timerActive": "定时器已激活",
"playbackWillPauseIn": "播放将在 ${duration} 后暂停",
"sleepTimerEndOfVideo": "当前视频结束时",
"sleepTimerStopAtHeader": "停止于",
"sleepTimerDurationHeader": "定时器",
"playbackWillPauseAtEnd": "播放将在此视频结束时暂停",
"stillWatching": "还在看吗?",
"pausingIn": "${seconds}秒后暂停",
"continueWatching": "继续",
@@ -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;
+43 -4
View File
@@ -15,6 +15,8 @@ class SleepTimerService extends ChangeNotifier {
Duration? _originalDuration;
VoidCallback? _onTimerComplete;
bool _needsRestart = false;
bool _restartAsEndOfVideo = false;
bool _endOfVideoArmed = false;
final StreamController<void> _completedController = StreamController<void>.broadcast();
final StreamController<void> _promptController = StreamController<void>.broadcast();
@@ -24,7 +26,11 @@ class SleepTimerService extends ChangeNotifier {
/// Emits when the timer fires and wants to show a "still watching?" prompt
Stream<void> 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);
}
}
@@ -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();
@@ -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));
},
);
},
),
),
],
);
}
}
@@ -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));
},
);
},
);
}
}
+114
View File
@@ -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<void>.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<void>.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<void>.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)
// ============================================================