From 53c94b14ccdf8da2e5cef518685d3c2c710e3f7f Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 16 Feb 2026 01:22:36 +0100 Subject: [PATCH] feat: add back button exit confirmation on Android TV close #467 --- lib/i18n/de.i18n.json | 10 ++++- lib/i18n/en.i18n.json | 10 ++++- lib/i18n/es.i18n.json | 10 ++++- lib/i18n/fr.i18n.json | 10 ++++- lib/i18n/it.i18n.json | 10 ++++- lib/i18n/ko.i18n.json | 10 ++++- lib/i18n/nl.i18n.json | 10 ++++- lib/i18n/strings.g.dart | 4 +- lib/i18n/strings_de.g.dart | 16 ++++++- lib/i18n/strings_en.g.dart | 28 +++++++++++- lib/i18n/strings_es.g.dart | 16 ++++++- lib/i18n/strings_fr.g.dart | 16 ++++++- lib/i18n/strings_it.g.dart | 16 ++++++- lib/i18n/strings_ko.g.dart | 16 ++++++- lib/i18n/strings_nl.g.dart | 16 ++++++- lib/i18n/strings_sv.g.dart | 16 ++++++- lib/i18n/strings_zh.g.dart | 16 ++++++- lib/i18n/sv.i18n.json | 10 ++++- lib/i18n/zh.i18n.json | 10 ++++- lib/screens/main_screen.dart | 21 ++++++++- lib/screens/settings/settings_screen.dart | 15 +++++++ lib/services/settings_service.dart | 11 +++++ lib/utils/dialogs.dart | 55 +++++++++++++++++++++++ 23 files changed, 313 insertions(+), 39 deletions(-) diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index b5c47b1d..d954783d 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -47,7 +47,11 @@ "mute": "Stumm", "ok": "OK", "loading": "Laden...", - "reconnect": "Erneut verbinden" + "reconnect": "Erneut verbinden", + "exitConfirmTitle": "App beenden?", + "exitConfirmMessage": "Möchtest du die App wirklich beenden?", + "dontAskAgain": "Nicht erneut fragen", + "exit": "Beenden" }, "screens": { "licenses": "Lizenzen", @@ -186,7 +190,9 @@ "matchContentFrameRate": "Inhalts-Bildrate anpassen", "matchContentFrameRateDescription": "Bildwiederholfrequenz des Displays an den Videoinhalt anpassen, reduziert Ruckeln und spart Akku", "requireProfileSelectionOnOpen": "Profil beim Öffnen abfragen", - "requireProfileSelectionOnOpenDescription": "Profilauswahl bei jedem Öffnen der App anzeigen" + "requireProfileSelectionOnOpenDescription": "Profilauswahl bei jedem Öffnen der App anzeigen", + "confirmExitOnBack": "Vor dem Beenden bestätigen", + "confirmExitOnBackDescription": "Bestätigungsdialog anzeigen, wenn Zurück zum Beenden der App gedrückt wird" }, "search": { "hint": "Filme, Serien, Musik suchen...", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 90c7e0bd..00dec1a0 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -47,7 +47,11 @@ "mute": "Mute", "ok": "OK", "loading": "Loading...", - "reconnect": "Reconnect" + "reconnect": "Reconnect", + "exitConfirmTitle": "Exit app?", + "exitConfirmMessage": "Are you sure you want to exit?", + "dontAskAgain": "Don't ask again", + "exit": "Exit" }, "screens": { "licenses": "Licenses", @@ -186,7 +190,9 @@ "matchContentFrameRate": "Match Content Frame Rate", "matchContentFrameRateDescription": "Adjust display refresh rate to match video content, reducing judder and saving battery", "requireProfileSelectionOnOpen": "Ask for profile on app open", - "requireProfileSelectionOnOpenDescription": "Show profile selection every time the app is opened" + "requireProfileSelectionOnOpenDescription": "Show profile selection every time the app is opened", + "confirmExitOnBack": "Confirm before exiting", + "confirmExitOnBackDescription": "Show a confirmation dialog when pressing back to exit the app" }, "search": { "hint": "Search movies, shows, music...", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index 34ec0f5b..2aa43f7f 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -47,7 +47,11 @@ "mute": "Silencio", "ok": "OK", "loading": "Cargando...", - "reconnect": "Reconectar" + "reconnect": "Reconectar", + "exitConfirmTitle": "¿Salir de la app?", + "exitConfirmMessage": "¿Estás seguro de que quieres salir?", + "dontAskAgain": "No volver a preguntar", + "exit": "Salir" }, "screens": { "licenses": "Licencias", @@ -186,7 +190,9 @@ "matchContentFrameRate": "Ajustar frecuencia de actualización", "matchContentFrameRateDescription": "Ajustar la frecuencia de actualización de la pantalla para que coincida con el video, reduciendo tirones y ahorrando batería", "requireProfileSelectionOnOpen": "Pedir perfil al abrir la app", - "requireProfileSelectionOnOpenDescription": "Mostrar selección de perfil cada vez que se abre la aplicación" + "requireProfileSelectionOnOpenDescription": "Mostrar selección de perfil cada vez que se abre la aplicación", + "confirmExitOnBack": "Confirmar antes de salir", + "confirmExitOnBackDescription": "Mostrar un diálogo de confirmación al presionar atrás para salir de la app" }, "search": { "hint": "Buscar películas, series, música...", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index 4333a603..f6f64dfa 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -47,7 +47,11 @@ "mute": "Muet", "ok": "OK", "loading": "Chargement...", - "reconnect": "Reconnecter" + "reconnect": "Reconnecter", + "exitConfirmTitle": "Quitter l'application ?", + "exitConfirmMessage": "Êtes-vous sûr de vouloir quitter ?", + "dontAskAgain": "Ne plus demander", + "exit": "Quitter" }, "screens": { "licenses": "Licenses", @@ -186,7 +190,9 @@ "matchContentFrameRate": "Fréquence d'images du contenu correspondant", "matchContentFrameRateDescription": "Ajustez la fréquence de rafraîchissement de l'écran en fonction du contenu vidéo, ce qui réduit les saccades et économise la batterie", "requireProfileSelectionOnOpen": "Demander le profil à l'ouverture", - "requireProfileSelectionOnOpenDescription": "Afficher la sélection de profil à chaque ouverture de l'application" + "requireProfileSelectionOnOpenDescription": "Afficher la sélection de profil à chaque ouverture de l'application", + "confirmExitOnBack": "Confirmer avant de quitter", + "confirmExitOnBackDescription": "Afficher une boîte de dialogue de confirmation en appuyant sur retour pour quitter" }, "search": { "hint": "Rechercher des films, des séries, de la musique...", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 1b7dc043..9e780caf 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -47,7 +47,11 @@ "mute": "Muto", "ok": "OK", "loading": "Caricamento...", - "reconnect": "Riconnetti" + "reconnect": "Riconnetti", + "exitConfirmTitle": "Uscire dall'app?", + "exitConfirmMessage": "Sei sicuro di voler uscire?", + "dontAskAgain": "Non chiedere più", + "exit": "Esci" }, "screens": { "licenses": "Licenze", @@ -186,7 +190,9 @@ "matchContentFrameRate": "Adatta frequenza fotogrammi", "matchContentFrameRateDescription": "Regola la frequenza di aggiornamento del display in base al contenuto video, riducendo i tremolii e risparmiando batteria", "requireProfileSelectionOnOpen": "Chiedi profilo all'apertura", - "requireProfileSelectionOnOpenDescription": "Mostra la selezione del profilo ogni volta che l'app viene aperta" + "requireProfileSelectionOnOpenDescription": "Mostra la selezione del profilo ogni volta che l'app viene aperta", + "confirmExitOnBack": "Conferma prima di uscire", + "confirmExitOnBackDescription": "Mostra una finestra di conferma quando si preme indietro per uscire dall'app" }, "search": { "hint": "Cerca film. spettacoli, musica...", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 578c4db4..c2fef135 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -47,7 +47,11 @@ "mute": "음소거", "ok": "확인", "loading": "로딩 중...", - "reconnect": "다시 연결" + "reconnect": "다시 연결", + "exitConfirmTitle": "앱을 종료하시겠습니까?", + "exitConfirmMessage": "정말 종료하시겠습니까?", + "dontAskAgain": "다시 묻지 않기", + "exit": "종료" }, "screens": { "licenses": "라이선스", @@ -186,7 +190,9 @@ "matchContentFrameRate": "콘텐츠 프레임 레이트 맞춤", "matchContentFrameRateDescription": "비디오 콘텐츠에 맞게 디스플레이 주사율을 조정하여 떨림을 줄이고 배터리를 절약합니다", "requireProfileSelectionOnOpen": "앱 실행 시 프로필 선택", - "requireProfileSelectionOnOpenDescription": "앱을 열 때마다 프로필 선택 화면을 표시합니다" + "requireProfileSelectionOnOpenDescription": "앱을 열 때마다 프로필 선택 화면을 표시합니다", + "confirmExitOnBack": "종료 전 확인", + "confirmExitOnBackDescription": "뒤로 버튼을 눌러 앱을 종료할 때 확인 대화상자를 표시합니다" }, "search": { "hint": "영화, 시리즈, 음악 등을 검색하세요...", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index 2a7fc7fc..96b77727 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -47,7 +47,11 @@ "mute": "Dempen", "ok": "OK", "loading": "Laden...", - "reconnect": "Opnieuw verbinden" + "reconnect": "Opnieuw verbinden", + "exitConfirmTitle": "App afsluiten?", + "exitConfirmMessage": "Weet je zeker dat je wilt afsluiten?", + "dontAskAgain": "Niet meer vragen", + "exit": "Afsluiten" }, "screens": { "licenses": "Licenties", @@ -186,7 +190,9 @@ "matchContentFrameRate": "Inhoudsframesnelheid afstemmen", "matchContentFrameRateDescription": "Pas de schermverversingssnelheid aan op de video-inhoud, vermindert haperingen en bespaart batterij", "requireProfileSelectionOnOpen": "Vraag om profiel bij openen", - "requireProfileSelectionOnOpenDescription": "Toon profielselectie telkens wanneer de app wordt geopend" + "requireProfileSelectionOnOpenDescription": "Toon profielselectie telkens wanneer de app wordt geopend", + "confirmExitOnBack": "Bevestigen voor afsluiten", + "confirmExitOnBackDescription": "Toon een bevestigingsvenster bij het drukken op terug om de app af te sluiten" }, "search": { "hint": "Zoek films, series, muziek...", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 718e59a1..2b0a2e07 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 9 -/// Strings: 6588 (732 per locale) +/// Strings: 6642 (738 per locale) /// -/// Built on 2026-02-15 at 04:13 UTC +/// Built on 2026-02-16 at 00:20 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index 2de42eaa..804fd01e 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -145,6 +145,10 @@ class _TranslationsCommonDe implements TranslationsCommonEn { @override String get ok => 'OK'; @override String get loading => 'Laden...'; @override String get reconnect => 'Erneut verbinden'; + @override String get exitConfirmTitle => 'App beenden?'; + @override String get exitConfirmMessage => 'Möchtest du die App wirklich beenden?'; + @override String get dontAskAgain => 'Nicht erneut fragen'; + @override String get exit => 'Beenden'; } // Path: screens @@ -305,6 +309,8 @@ class _TranslationsSettingsDe implements TranslationsSettingsEn { @override String get matchContentFrameRateDescription => 'Bildwiederholfrequenz des Displays an den Videoinhalt anpassen, reduziert Ruckeln und spart Akku'; @override String get requireProfileSelectionOnOpen => 'Profil beim Öffnen abfragen'; @override String get requireProfileSelectionOnOpenDescription => 'Profilauswahl bei jedem Öffnen der App anzeigen'; + @override String get confirmExitOnBack => 'Vor dem Beenden bestätigen'; + @override String get confirmExitOnBackDescription => 'Bestätigungsdialog anzeigen, wenn Zurück zum Beenden der App gedrückt wird'; } // Path: search @@ -1263,6 +1269,10 @@ extension on TranslationsDe { 'common.ok' => 'OK', 'common.loading' => 'Laden...', 'common.reconnect' => 'Erneut verbinden', + 'common.exitConfirmTitle' => 'App beenden?', + 'common.exitConfirmMessage' => 'Möchtest du die App wirklich beenden?', + 'common.dontAskAgain' => 'Nicht erneut fragen', + 'common.exit' => 'Beenden', 'screens.licenses' => 'Lizenzen', 'screens.switchProfile' => 'Profil wechseln', 'screens.subtitleStyling' => 'Untertitel-Stil', @@ -1396,6 +1406,8 @@ extension on TranslationsDe { 'settings.matchContentFrameRateDescription' => 'Bildwiederholfrequenz des Displays an den Videoinhalt anpassen, reduziert Ruckeln und spart Akku', 'settings.requireProfileSelectionOnOpen' => 'Profil beim Öffnen abfragen', 'settings.requireProfileSelectionOnOpenDescription' => 'Profilauswahl bei jedem Öffnen der App anzeigen', + 'settings.confirmExitOnBack' => 'Vor dem Beenden bestätigen', + 'settings.confirmExitOnBackDescription' => 'Bestätigungsdialog anzeigen, wenn Zurück zum Beenden der App gedrückt wird', 'search.hint' => 'Filme, Serien, Musik suchen...', 'search.tryDifferentTerm' => 'Anderen Suchbegriff versuchen', 'search.searchYourMedia' => 'In den eigenen Medien suchen', @@ -1725,14 +1737,14 @@ extension on TranslationsDe { 'liveTv.evening' => 'Abend', 'liveTv.lateNight' => 'Spätnacht', 'liveTv.whatsOn' => 'Jetzt im TV', + _ => null, + } ?? switch (path) { 'liveTv.watchChannel' => 'Kanal ansehen', 'downloads.title' => 'Downloads', 'downloads.manage' => 'Verwalten', 'downloads.tvShows' => 'Serien', 'downloads.movies' => 'Filme', 'downloads.noDownloads' => 'Noch keine Downloads', - _ => null, - } ?? switch (path) { 'downloads.noDownloadsDescription' => 'Heruntergeladene Inhalte werden hier für die Offline-Wiedergabe angezeigt', 'downloads.downloadNow' => 'Herunterladen', 'downloads.deleteDownload' => 'Download löschen', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index 3873b7bb..a3230baa 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -236,6 +236,18 @@ class TranslationsCommonEn { /// en: 'Reconnect' String get reconnect => 'Reconnect'; + + /// en: 'Exit app?' + String get exitConfirmTitle => 'Exit app?'; + + /// en: 'Are you sure you want to exit?' + String get exitConfirmMessage => 'Are you sure you want to exit?'; + + /// en: 'Don't ask again' + String get dontAskAgain => 'Don\'t ask again'; + + /// en: 'Exit' + String get exit => 'Exit'; } // Path: screens @@ -662,6 +674,12 @@ class TranslationsSettingsEn { /// en: 'Show profile selection every time the app is opened' String get requireProfileSelectionOnOpenDescription => 'Show profile selection every time the app is opened'; + + /// en: 'Confirm before exiting' + String get confirmExitOnBack => 'Confirm before exiting'; + + /// en: 'Show a confirmation dialog when pressing back to exit the app' + String get confirmExitOnBackDescription => 'Show a confirmation dialog when pressing back to exit the app'; } // Path: search @@ -2734,6 +2752,10 @@ extension on Translations { 'common.ok' => 'OK', 'common.loading' => 'Loading...', 'common.reconnect' => 'Reconnect', + 'common.exitConfirmTitle' => 'Exit app?', + 'common.exitConfirmMessage' => 'Are you sure you want to exit?', + 'common.dontAskAgain' => 'Don\'t ask again', + 'common.exit' => 'Exit', 'screens.licenses' => 'Licenses', 'screens.switchProfile' => 'Switch Profile', 'screens.subtitleStyling' => 'Subtitle Styling', @@ -2867,6 +2889,8 @@ extension on Translations { 'settings.matchContentFrameRateDescription' => 'Adjust display refresh rate to match video content, reducing judder and saving battery', 'settings.requireProfileSelectionOnOpen' => 'Ask for profile on app open', 'settings.requireProfileSelectionOnOpenDescription' => 'Show profile selection every time the app is opened', + 'settings.confirmExitOnBack' => 'Confirm before exiting', + 'settings.confirmExitOnBackDescription' => 'Show a confirmation dialog when pressing back to exit the app', 'search.hint' => 'Search movies, shows, music...', 'search.tryDifferentTerm' => 'Try a different search term', 'search.searchYourMedia' => 'Search your media', @@ -3196,14 +3220,14 @@ extension on Translations { 'liveTv.evening' => 'Evening', 'liveTv.lateNight' => 'Late Night', 'liveTv.whatsOn' => 'What\'s On', + _ => null, + } ?? switch (path) { 'liveTv.watchChannel' => 'Watch Channel', 'collections.title' => 'Collections', 'collections.collection' => 'Collection', 'collections.empty' => 'Collection is empty', 'collections.unknownLibrarySection' => 'Cannot delete: Unknown library section', 'collections.deleteCollection' => 'Delete Collection', - _ => null, - } ?? switch (path) { 'collections.deleteConfirm' => ({required Object title}) => 'Are you sure you want to delete "${title}"? This action cannot be undone.', 'collections.deleted' => 'Collection deleted', 'collections.deleteFailed' => 'Failed to delete collection', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index babc16c0..781bbfaa 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -145,6 +145,10 @@ class _TranslationsCommonEs implements TranslationsCommonEn { @override String get ok => 'OK'; @override String get loading => 'Cargando...'; @override String get reconnect => 'Reconectar'; + @override String get exitConfirmTitle => '¿Salir de la app?'; + @override String get exitConfirmMessage => '¿Estás seguro de que quieres salir?'; + @override String get dontAskAgain => 'No volver a preguntar'; + @override String get exit => 'Salir'; } // Path: screens @@ -305,6 +309,8 @@ class _TranslationsSettingsEs implements TranslationsSettingsEn { @override String get matchContentFrameRateDescription => 'Ajustar la frecuencia de actualización de la pantalla para que coincida con el video, reduciendo tirones y ahorrando batería'; @override String get requireProfileSelectionOnOpen => 'Pedir perfil al abrir la app'; @override String get requireProfileSelectionOnOpenDescription => 'Mostrar selección de perfil cada vez que se abre la aplicación'; + @override String get confirmExitOnBack => 'Confirmar antes de salir'; + @override String get confirmExitOnBackDescription => 'Mostrar un diálogo de confirmación al presionar atrás para salir de la app'; } // Path: search @@ -1263,6 +1269,10 @@ extension on TranslationsEs { 'common.ok' => 'OK', 'common.loading' => 'Cargando...', 'common.reconnect' => 'Reconectar', + 'common.exitConfirmTitle' => '¿Salir de la app?', + 'common.exitConfirmMessage' => '¿Estás seguro de que quieres salir?', + 'common.dontAskAgain' => 'No volver a preguntar', + 'common.exit' => 'Salir', 'screens.licenses' => 'Licencias', 'screens.switchProfile' => 'Cambiar Perfil', 'screens.subtitleStyling' => 'Estilo de Subtítulos', @@ -1396,6 +1406,8 @@ extension on TranslationsEs { 'settings.matchContentFrameRateDescription' => 'Ajustar la frecuencia de actualización de la pantalla para que coincida con el video, reduciendo tirones y ahorrando batería', 'settings.requireProfileSelectionOnOpen' => 'Pedir perfil al abrir la app', 'settings.requireProfileSelectionOnOpenDescription' => 'Mostrar selección de perfil cada vez que se abre la aplicación', + 'settings.confirmExitOnBack' => 'Confirmar antes de salir', + 'settings.confirmExitOnBackDescription' => 'Mostrar un diálogo de confirmación al presionar atrás para salir de la app', 'search.hint' => 'Buscar películas, series, música...', 'search.tryDifferentTerm' => 'Prueba con un término de búsqueda diferente', 'search.searchYourMedia' => 'Busca en tu contenido', @@ -1725,14 +1737,14 @@ extension on TranslationsEs { 'liveTv.evening' => 'Noche', 'liveTv.lateNight' => 'Trasnoche', 'liveTv.whatsOn' => 'En emisión', + _ => null, + } ?? switch (path) { 'liveTv.watchChannel' => 'Ver canal', 'collections.title' => 'Colecciones', 'collections.collection' => 'Colección', 'collections.empty' => 'La colección está vacía', 'collections.unknownLibrarySection' => 'No se puede eliminar: Sección de biblioteca desconocida', 'collections.deleteCollection' => 'Eliminar Colección', - _ => null, - } ?? switch (path) { 'collections.deleteConfirm' => ({required Object title}) => '¿Estás seguro de que quieres eliminar "${title}"? Esta acción no se puede deshacer.', 'collections.deleted' => 'Colección eliminada', 'collections.deleteFailed' => 'Error al eliminar la colección', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index 6db3c6bf..5ecd065b 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -145,6 +145,10 @@ class _TranslationsCommonFr implements TranslationsCommonEn { @override String get ok => 'OK'; @override String get loading => 'Chargement...'; @override String get reconnect => 'Reconnecter'; + @override String get exitConfirmTitle => 'Quitter l\'application ?'; + @override String get exitConfirmMessage => 'Êtes-vous sûr de vouloir quitter ?'; + @override String get dontAskAgain => 'Ne plus demander'; + @override String get exit => 'Quitter'; } // Path: screens @@ -305,6 +309,8 @@ class _TranslationsSettingsFr implements TranslationsSettingsEn { @override String get matchContentFrameRateDescription => 'Ajustez la fréquence de rafraîchissement de l\'écran en fonction du contenu vidéo, ce qui réduit les saccades et économise la batterie'; @override String get requireProfileSelectionOnOpen => 'Demander le profil à l\'ouverture'; @override String get requireProfileSelectionOnOpenDescription => 'Afficher la sélection de profil à chaque ouverture de l\'application'; + @override String get confirmExitOnBack => 'Confirmer avant de quitter'; + @override String get confirmExitOnBackDescription => 'Afficher une boîte de dialogue de confirmation en appuyant sur retour pour quitter'; } // Path: search @@ -1263,6 +1269,10 @@ extension on TranslationsFr { 'common.ok' => 'OK', 'common.loading' => 'Chargement...', 'common.reconnect' => 'Reconnecter', + 'common.exitConfirmTitle' => 'Quitter l\'application ?', + 'common.exitConfirmMessage' => 'Êtes-vous sûr de vouloir quitter ?', + 'common.dontAskAgain' => 'Ne plus demander', + 'common.exit' => 'Quitter', 'screens.licenses' => 'Licenses', 'screens.switchProfile' => 'Changer de profil', 'screens.subtitleStyling' => 'Configuration des sous-titres', @@ -1396,6 +1406,8 @@ extension on TranslationsFr { 'settings.matchContentFrameRateDescription' => 'Ajustez la fréquence de rafraîchissement de l\'écran en fonction du contenu vidéo, ce qui réduit les saccades et économise la batterie', 'settings.requireProfileSelectionOnOpen' => 'Demander le profil à l\'ouverture', 'settings.requireProfileSelectionOnOpenDescription' => 'Afficher la sélection de profil à chaque ouverture de l\'application', + 'settings.confirmExitOnBack' => 'Confirmer avant de quitter', + 'settings.confirmExitOnBackDescription' => 'Afficher une boîte de dialogue de confirmation en appuyant sur retour pour quitter', 'search.hint' => 'Rechercher des films, des séries, de la musique...', 'search.tryDifferentTerm' => 'Essayez un autre terme de recherche', 'search.searchYourMedia' => 'Rechercher dans vos médias', @@ -1725,14 +1737,14 @@ extension on TranslationsFr { 'liveTv.evening' => 'Soirée', 'liveTv.lateNight' => 'Nuit tardive', 'liveTv.whatsOn' => 'En ce moment', + _ => null, + } ?? switch (path) { 'liveTv.watchChannel' => 'Regarder la chaîne', 'collections.title' => 'Collections', 'collections.collection' => 'Collection', 'collections.empty' => 'La collection est vide', 'collections.unknownLibrarySection' => 'Impossible de supprimer : section de bibliothèque inconnue', 'collections.deleteCollection' => 'Supprimer la collection', - _ => null, - } ?? switch (path) { 'collections.deleteConfirm' => ({required Object title}) => 'Êtes-vous sûr de vouloir supprimer "${title}" ? Cette action ne peut pas être annulée.', 'collections.deleted' => 'Collection supprimée', 'collections.deleteFailed' => 'Échec de la suppression de la collection', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index f00ad2e3..8691243b 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -145,6 +145,10 @@ class _TranslationsCommonIt implements TranslationsCommonEn { @override String get ok => 'OK'; @override String get loading => 'Caricamento...'; @override String get reconnect => 'Riconnetti'; + @override String get exitConfirmTitle => 'Uscire dall\'app?'; + @override String get exitConfirmMessage => 'Sei sicuro di voler uscire?'; + @override String get dontAskAgain => 'Non chiedere più'; + @override String get exit => 'Esci'; } // Path: screens @@ -305,6 +309,8 @@ class _TranslationsSettingsIt implements TranslationsSettingsEn { @override String get matchContentFrameRateDescription => 'Regola la frequenza di aggiornamento del display in base al contenuto video, riducendo i tremolii e risparmiando batteria'; @override String get requireProfileSelectionOnOpen => 'Chiedi profilo all\'apertura'; @override String get requireProfileSelectionOnOpenDescription => 'Mostra la selezione del profilo ogni volta che l\'app viene aperta'; + @override String get confirmExitOnBack => 'Conferma prima di uscire'; + @override String get confirmExitOnBackDescription => 'Mostra una finestra di conferma quando si preme indietro per uscire dall\'app'; } // Path: search @@ -1263,6 +1269,10 @@ extension on TranslationsIt { 'common.ok' => 'OK', 'common.loading' => 'Caricamento...', 'common.reconnect' => 'Riconnetti', + 'common.exitConfirmTitle' => 'Uscire dall\'app?', + 'common.exitConfirmMessage' => 'Sei sicuro di voler uscire?', + 'common.dontAskAgain' => 'Non chiedere più', + 'common.exit' => 'Esci', 'screens.licenses' => 'Licenze', 'screens.switchProfile' => 'Cambia profilo', 'screens.subtitleStyling' => 'Stile sottotitoli', @@ -1396,6 +1406,8 @@ extension on TranslationsIt { 'settings.matchContentFrameRateDescription' => 'Regola la frequenza di aggiornamento del display in base al contenuto video, riducendo i tremolii e risparmiando batteria', 'settings.requireProfileSelectionOnOpen' => 'Chiedi profilo all\'apertura', 'settings.requireProfileSelectionOnOpenDescription' => 'Mostra la selezione del profilo ogni volta che l\'app viene aperta', + 'settings.confirmExitOnBack' => 'Conferma prima di uscire', + 'settings.confirmExitOnBackDescription' => 'Mostra una finestra di conferma quando si preme indietro per uscire dall\'app', 'search.hint' => 'Cerca film. spettacoli, musica...', 'search.tryDifferentTerm' => 'Prova altri termini di ricerca', 'search.searchYourMedia' => 'Cerca nei tuoi media', @@ -1725,14 +1737,14 @@ extension on TranslationsIt { 'liveTv.evening' => 'Sera', 'liveTv.lateNight' => 'Notte tarda', 'liveTv.whatsOn' => 'In onda ora', + _ => null, + } ?? switch (path) { 'liveTv.watchChannel' => 'Guarda canale', 'downloads.title' => 'Download', 'downloads.manage' => 'Gestisci', 'downloads.tvShows' => 'Serie TV', 'downloads.movies' => 'Film', 'downloads.noDownloads' => 'Nessun download', - _ => null, - } ?? switch (path) { 'downloads.noDownloadsDescription' => 'I contenuti scaricati appariranno qui per la visualizzazione offline', 'downloads.downloadNow' => 'Scarica', 'downloads.deleteDownload' => 'Elimina download', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index e10ad18f..4e0728bd 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -145,6 +145,10 @@ class _TranslationsCommonKo implements TranslationsCommonEn { @override String get ok => '확인'; @override String get loading => '로딩 중...'; @override String get reconnect => '다시 연결'; + @override String get exitConfirmTitle => '앱을 종료하시겠습니까?'; + @override String get exitConfirmMessage => '정말 종료하시겠습니까?'; + @override String get dontAskAgain => '다시 묻지 않기'; + @override String get exit => '종료'; } // Path: screens @@ -305,6 +309,8 @@ class _TranslationsSettingsKo implements TranslationsSettingsEn { @override String get matchContentFrameRateDescription => '비디오 콘텐츠에 맞게 디스플레이 주사율을 조정하여 떨림을 줄이고 배터리를 절약합니다'; @override String get requireProfileSelectionOnOpen => '앱 실행 시 프로필 선택'; @override String get requireProfileSelectionOnOpenDescription => '앱을 열 때마다 프로필 선택 화면을 표시합니다'; + @override String get confirmExitOnBack => '종료 전 확인'; + @override String get confirmExitOnBackDescription => '뒤로 버튼을 눌러 앱을 종료할 때 확인 대화상자를 표시합니다'; } // Path: search @@ -1263,6 +1269,10 @@ extension on TranslationsKo { 'common.ok' => '확인', 'common.loading' => '로딩 중...', 'common.reconnect' => '다시 연결', + 'common.exitConfirmTitle' => '앱을 종료하시겠습니까?', + 'common.exitConfirmMessage' => '정말 종료하시겠습니까?', + 'common.dontAskAgain' => '다시 묻지 않기', + 'common.exit' => '종료', 'screens.licenses' => '라이선스', 'screens.switchProfile' => '프로필 전환', 'screens.subtitleStyling' => '자막 스타일 설정', @@ -1396,6 +1406,8 @@ extension on TranslationsKo { 'settings.matchContentFrameRateDescription' => '비디오 콘텐츠에 맞게 디스플레이 주사율을 조정하여 떨림을 줄이고 배터리를 절약합니다', 'settings.requireProfileSelectionOnOpen' => '앱 실행 시 프로필 선택', 'settings.requireProfileSelectionOnOpenDescription' => '앱을 열 때마다 프로필 선택 화면을 표시합니다', + 'settings.confirmExitOnBack' => '종료 전 확인', + 'settings.confirmExitOnBackDescription' => '뒤로 버튼을 눌러 앱을 종료할 때 확인 대화상자를 표시합니다', 'search.hint' => '영화, 시리즈, 음악 등을 검색하세요...', 'search.tryDifferentTerm' => '다른 검색어를 시도해 보세요', 'search.searchYourMedia' => '미디어 검색', @@ -1725,14 +1737,14 @@ extension on TranslationsKo { 'liveTv.evening' => '저녁', 'liveTv.lateNight' => '심야 방송', 'liveTv.whatsOn' => '지금 방송 중', + _ => null, + } ?? switch (path) { 'liveTv.watchChannel' => '채널 시청', 'collections.title' => '컬렉션', 'collections.collection' => '컬렉션', 'collections.empty' => '컬렉션이 비어 있습니다', 'collections.unknownLibrarySection' => '삭제할 수 없습니다: 알 수 없는 미디어 라이브러리 섹션입니다', 'collections.deleteCollection' => '컬렉션 삭제', - _ => null, - } ?? switch (path) { 'collections.deleteConfirm' => ({required Object title}) => '"${title}"을(를) 삭제 하시겠습니까? 이 작업은 되돌릴 수 없습니다.', 'collections.deleted' => '컬렉션 삭제됨', 'collections.deleteFailed' => '컬렉션 삭제 실패', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index 1af29136..de11dcc3 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -145,6 +145,10 @@ class _TranslationsCommonNl implements TranslationsCommonEn { @override String get ok => 'OK'; @override String get loading => 'Laden...'; @override String get reconnect => 'Opnieuw verbinden'; + @override String get exitConfirmTitle => 'App afsluiten?'; + @override String get exitConfirmMessage => 'Weet je zeker dat je wilt afsluiten?'; + @override String get dontAskAgain => 'Niet meer vragen'; + @override String get exit => 'Afsluiten'; } // Path: screens @@ -305,6 +309,8 @@ class _TranslationsSettingsNl implements TranslationsSettingsEn { @override String get matchContentFrameRateDescription => 'Pas de schermverversingssnelheid aan op de video-inhoud, vermindert haperingen en bespaart batterij'; @override String get requireProfileSelectionOnOpen => 'Vraag om profiel bij openen'; @override String get requireProfileSelectionOnOpenDescription => 'Toon profielselectie telkens wanneer de app wordt geopend'; + @override String get confirmExitOnBack => 'Bevestigen voor afsluiten'; + @override String get confirmExitOnBackDescription => 'Toon een bevestigingsvenster bij het drukken op terug om de app af te sluiten'; } // Path: search @@ -1263,6 +1269,10 @@ extension on TranslationsNl { 'common.ok' => 'OK', 'common.loading' => 'Laden...', 'common.reconnect' => 'Opnieuw verbinden', + 'common.exitConfirmTitle' => 'App afsluiten?', + 'common.exitConfirmMessage' => 'Weet je zeker dat je wilt afsluiten?', + 'common.dontAskAgain' => 'Niet meer vragen', + 'common.exit' => 'Afsluiten', 'screens.licenses' => 'Licenties', 'screens.switchProfile' => 'Wissel van profiel', 'screens.subtitleStyling' => 'Ondertitel opmaak', @@ -1396,6 +1406,8 @@ extension on TranslationsNl { 'settings.matchContentFrameRateDescription' => 'Pas de schermverversingssnelheid aan op de video-inhoud, vermindert haperingen en bespaart batterij', 'settings.requireProfileSelectionOnOpen' => 'Vraag om profiel bij openen', 'settings.requireProfileSelectionOnOpenDescription' => 'Toon profielselectie telkens wanneer de app wordt geopend', + 'settings.confirmExitOnBack' => 'Bevestigen voor afsluiten', + 'settings.confirmExitOnBackDescription' => 'Toon een bevestigingsvenster bij het drukken op terug om de app af te sluiten', 'search.hint' => 'Zoek films, series, muziek...', 'search.tryDifferentTerm' => 'Probeer een andere zoekterm', 'search.searchYourMedia' => 'Zoek in je media', @@ -1725,14 +1737,14 @@ extension on TranslationsNl { 'liveTv.evening' => 'Avond', 'liveTv.lateNight' => 'Late avond', 'liveTv.whatsOn' => 'Nu op TV', + _ => null, + } ?? switch (path) { 'liveTv.watchChannel' => 'Kanaal bekijken', 'downloads.title' => 'Downloads', 'downloads.manage' => 'Beheren', 'downloads.tvShows' => 'Series', 'downloads.movies' => 'Films', 'downloads.noDownloads' => 'Nog geen downloads', - _ => null, - } ?? switch (path) { 'downloads.noDownloadsDescription' => 'Gedownloade content verschijnt hier voor offline weergave', 'downloads.downloadNow' => 'Download', 'downloads.deleteDownload' => 'Download verwijderen', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index c584561b..f83c15e5 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -145,6 +145,10 @@ class _TranslationsCommonSv implements TranslationsCommonEn { @override String get ok => 'OK'; @override String get loading => 'Laddar...'; @override String get reconnect => 'Återanslut'; + @override String get exitConfirmTitle => 'Avsluta appen?'; + @override String get exitConfirmMessage => 'Är du säker på att du vill avsluta?'; + @override String get dontAskAgain => 'Fråga inte igen'; + @override String get exit => 'Avsluta'; } // Path: screens @@ -305,6 +309,8 @@ class _TranslationsSettingsSv implements TranslationsSettingsEn { @override String get matchContentFrameRateDescription => 'Justera skärmens uppdateringsfrekvens för att matcha videoinnehållet, minskar hackighet och sparar batteri'; @override String get requireProfileSelectionOnOpen => 'Fråga efter profil vid appstart'; @override String get requireProfileSelectionOnOpenDescription => 'Visa profilval varje gång appen öppnas'; + @override String get confirmExitOnBack => 'Bekräfta innan avslut'; + @override String get confirmExitOnBackDescription => 'Visa en bekräftelsedialog när du trycker tillbaka för att avsluta appen'; } // Path: search @@ -1263,6 +1269,10 @@ extension on TranslationsSv { 'common.ok' => 'OK', 'common.loading' => 'Laddar...', 'common.reconnect' => 'Återanslut', + 'common.exitConfirmTitle' => 'Avsluta appen?', + 'common.exitConfirmMessage' => 'Är du säker på att du vill avsluta?', + 'common.dontAskAgain' => 'Fråga inte igen', + 'common.exit' => 'Avsluta', 'screens.licenses' => 'Licenser', 'screens.switchProfile' => 'Byt profil', 'screens.subtitleStyling' => 'Undertext-styling', @@ -1396,6 +1406,8 @@ extension on TranslationsSv { 'settings.matchContentFrameRateDescription' => 'Justera skärmens uppdateringsfrekvens för att matcha videoinnehållet, minskar hackighet och sparar batteri', 'settings.requireProfileSelectionOnOpen' => 'Fråga efter profil vid appstart', 'settings.requireProfileSelectionOnOpenDescription' => 'Visa profilval varje gång appen öppnas', + 'settings.confirmExitOnBack' => 'Bekräfta innan avslut', + 'settings.confirmExitOnBackDescription' => 'Visa en bekräftelsedialog när du trycker tillbaka för att avsluta appen', 'search.hint' => 'Sök filmer, serier, musik...', 'search.tryDifferentTerm' => 'Prova en annan sökterm', 'search.searchYourMedia' => 'Sök i dina media', @@ -1725,14 +1737,14 @@ extension on TranslationsSv { 'liveTv.evening' => 'Kväll', 'liveTv.lateNight' => 'Sen kväll', 'liveTv.whatsOn' => 'På TV nu', + _ => null, + } ?? switch (path) { 'liveTv.watchChannel' => 'Titta på kanal', 'downloads.title' => 'Nedladdningar', 'downloads.manage' => 'Hantera', 'downloads.tvShows' => 'TV-serier', 'downloads.movies' => 'Filmer', 'downloads.noDownloads' => 'Inga nedladdningar ännu', - _ => null, - } ?? switch (path) { 'downloads.noDownloadsDescription' => 'Nedladdat innehåll visas här för offline-visning', 'downloads.downloadNow' => 'Ladda ner', 'downloads.deleteDownload' => 'Ta bort nedladdning', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index 1690b1dd..32fe7d62 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -145,6 +145,10 @@ class _TranslationsCommonZh implements TranslationsCommonEn { @override String get ok => '确定'; @override String get loading => '加载中...'; @override String get reconnect => '重新连接'; + @override String get exitConfirmTitle => '退出应用?'; + @override String get exitConfirmMessage => '确定要退出吗?'; + @override String get dontAskAgain => '不再询问'; + @override String get exit => '退出'; } // Path: screens @@ -305,6 +309,8 @@ class _TranslationsSettingsZh implements TranslationsSettingsEn { @override String get matchContentFrameRateDescription => '调整显示刷新率以匹配视频内容,减少画面抖动并节省电量'; @override String get requireProfileSelectionOnOpen => '打开应用时询问配置文件'; @override String get requireProfileSelectionOnOpenDescription => '每次打开应用时显示配置文件选择'; + @override String get confirmExitOnBack => '退出前确认'; + @override String get confirmExitOnBackDescription => '按返回键退出应用时显示确认对话框'; } // Path: search @@ -1263,6 +1269,10 @@ extension on TranslationsZh { 'common.ok' => '确定', 'common.loading' => '加载中...', 'common.reconnect' => '重新连接', + 'common.exitConfirmTitle' => '退出应用?', + 'common.exitConfirmMessage' => '确定要退出吗?', + 'common.dontAskAgain' => '不再询问', + 'common.exit' => '退出', 'screens.licenses' => '许可证', 'screens.switchProfile' => '切换用户', 'screens.subtitleStyling' => '字幕样式', @@ -1396,6 +1406,8 @@ extension on TranslationsZh { 'settings.matchContentFrameRateDescription' => '调整显示刷新率以匹配视频内容,减少画面抖动并节省电量', 'settings.requireProfileSelectionOnOpen' => '打开应用时询问配置文件', 'settings.requireProfileSelectionOnOpenDescription' => '每次打开应用时显示配置文件选择', + 'settings.confirmExitOnBack' => '退出前确认', + 'settings.confirmExitOnBackDescription' => '按返回键退出应用时显示确认对话框', 'search.hint' => '搜索电影、系列、音乐...', 'search.tryDifferentTerm' => '尝试不同的搜索词', 'search.searchYourMedia' => '搜索媒体', @@ -1725,14 +1737,14 @@ extension on TranslationsZh { 'liveTv.evening' => '晚上', 'liveTv.lateNight' => '深夜', 'liveTv.whatsOn' => '正在播出', + _ => null, + } ?? switch (path) { 'liveTv.watchChannel' => '观看频道', 'downloads.title' => '下载', 'downloads.manage' => '管理', 'downloads.tvShows' => '电视剧', 'downloads.movies' => '电影', 'downloads.noDownloads' => '暂无下载', - _ => null, - } ?? switch (path) { 'downloads.noDownloadsDescription' => '下载的内容将在此处显示以供离线观看', 'downloads.downloadNow' => '下载', 'downloads.deleteDownload' => '删除下载', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 0f45573e..d4a719e0 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -47,7 +47,11 @@ "mute": "Ljud av", "ok": "OK", "loading": "Laddar...", - "reconnect": "Återanslut" + "reconnect": "Återanslut", + "exitConfirmTitle": "Avsluta appen?", + "exitConfirmMessage": "Är du säker på att du vill avsluta?", + "dontAskAgain": "Fråga inte igen", + "exit": "Avsluta" }, "screens": { "licenses": "Licenser", @@ -186,7 +190,9 @@ "matchContentFrameRate": "Matcha innehållets bildfrekvens", "matchContentFrameRateDescription": "Justera skärmens uppdateringsfrekvens för att matcha videoinnehållet, minskar hackighet och sparar batteri", "requireProfileSelectionOnOpen": "Fråga efter profil vid appstart", - "requireProfileSelectionOnOpenDescription": "Visa profilval varje gång appen öppnas" + "requireProfileSelectionOnOpenDescription": "Visa profilval varje gång appen öppnas", + "confirmExitOnBack": "Bekräfta innan avslut", + "confirmExitOnBackDescription": "Visa en bekräftelsedialog när du trycker tillbaka för att avsluta appen" }, "search": { "hint": "Sök filmer, serier, musik...", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index c99ad198..41b9b450 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -47,7 +47,11 @@ "mute": "静音", "ok": "确定", "loading": "加载中...", - "reconnect": "重新连接" + "reconnect": "重新连接", + "exitConfirmTitle": "退出应用?", + "exitConfirmMessage": "确定要退出吗?", + "dontAskAgain": "不再询问", + "exit": "退出" }, "screens": { "licenses": "许可证", @@ -186,7 +190,9 @@ "matchContentFrameRate": "匹配内容帧率", "matchContentFrameRateDescription": "调整显示刷新率以匹配视频内容,减少画面抖动并节省电量", "requireProfileSelectionOnOpen": "打开应用时询问配置文件", - "requireProfileSelectionOnOpenDescription": "每次打开应用时显示配置文件选择" + "requireProfileSelectionOnOpenDescription": "每次打开应用时显示配置文件选择", + "confirmExitOnBack": "退出前确认", + "confirmExitOnBackDescription": "按返回键退出应用时显示确认对话框" }, "search": { "hint": "搜索电影、系列、音乐...", diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 5f8760f0..43f08f06 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -10,6 +10,7 @@ import '../../services/plex_client.dart'; import '../i18n/strings.g.dart'; import '../services/update_service.dart'; import '../utils/app_logger.dart'; +import '../utils/dialogs.dart'; import '../utils/provider_extensions.dart'; import '../utils/platform_detector.dart'; import '../utils/video_player_navigation.dart'; @@ -702,7 +703,25 @@ class _MainScreenState extends State with RouteAware, WindowListener } // Sidebar focused → exit app - return handleBackKeyAction(event, () => SystemNavigator.pop()); + return handleBackKeyAction(event, () async { + if (PlatformDetector.isTV()) { + final settings = await SettingsService.getInstance(); + if (settings.getConfirmExitOnBack() && mounted) { + final result = await showConfirmDialogWithCheckbox( + context, + title: t.common.exitConfirmTitle, + message: t.common.exitConfirmMessage, + confirmText: t.common.exit, + checkboxLabel: t.common.dontAskAgain, + ); + if (result.checked) { + await settings.setConfirmExitOnBack(false); + } + if (!result.confirmed) return; + } + } + SystemNavigator.pop(); + }); } @override diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index 3fca243d..4c1837f2 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -68,6 +68,7 @@ class _SettingsScreenState extends State with FocusableTab { static const _kAlwaysKeepSidebarOpen = 'always_keep_sidebar_open'; static const _kShowUnwatchedCount = 'show_unwatched_count'; static const _kRequireProfileSelectionOnOpen = 'require_profile_selection_on_open'; + static const _kConfirmExitOnBack = 'confirm_exit_on_back'; static const _kPlayerBackend = 'player_backend'; static const _kExternalPlayer = 'external_player'; static const _kHardwareDecoding = 'hardware_decoding'; @@ -118,6 +119,7 @@ class _SettingsScreenState extends State with FocusableTab { bool _useExoPlayer = true; // Android only: ExoPlayer vs MPV bool _requireProfileSelectionOnOpen = false; bool _useExternalPlayer = false; + bool _confirmExitOnBack = true; String _selectedExternalPlayerName = ''; // Update checking state @@ -190,6 +192,7 @@ class _SettingsScreenState extends State with FocusableTab { _requireProfileSelectionOnOpen = _settingsService.getRequireProfileSelectionOnOpen(); _useExternalPlayer = _settingsService.getUseExternalPlayer(); _selectedExternalPlayerName = _settingsService.getSelectedExternalPlayer().name; + _confirmExitOnBack = _settingsService.getConfirmExitOnBack(); _isLoading = false; }); } @@ -390,6 +393,18 @@ class _SettingsScreenState extends State with FocusableTab { ); }, ), + if (PlatformDetector.isTV()) + SwitchListTile( + focusNode: _focusTracker.get(_kConfirmExitOnBack), + secondary: const AppIcon(Symbols.exit_to_app_rounded, fill: 1), + title: Text(t.settings.confirmExitOnBack), + subtitle: Text(t.settings.confirmExitOnBackDescription), + value: _confirmExitOnBack, + onChanged: (value) async { + setState(() => _confirmExitOnBack = value); + await _settingsService.setConfirmExitOnBack(value); + }, + ), ], ), ); diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 78df3a31..0167a4fa 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -74,6 +74,7 @@ class SettingsService extends BaseSharedPreferencesService { static const String _keyUseExternalPlayer = 'use_external_player'; static const String _keySelectedExternalPlayer = 'selected_external_player'; static const String _keyCustomExternalPlayers = 'custom_external_players'; + static const String _keyConfirmExitOnBack = 'confirm_exit_on_back'; SettingsService._(); @@ -1107,6 +1108,15 @@ class SettingsService extends BaseSharedPreferencesService { } } + // Confirm Exit on Back (Android TV) + Future setConfirmExitOnBack(bool value) async { + await prefs.setBool(_keyConfirmExitOnBack, value); + } + + bool getConfirmExitOnBack() { + return prefs.getBool(_keyConfirmExitOnBack) ?? true; // Default: enabled + } + // Reset all settings to defaults Future resetAllSettings() async { await Future.wait([ @@ -1160,6 +1170,7 @@ class SettingsService extends BaseSharedPreferencesService { prefs.remove(_keyUseExternalPlayer), prefs.remove(_keySelectedExternalPlayer), prefs.remove(_keyCustomExternalPlayers), + prefs.remove(_keyConfirmExitOnBack), ]); } diff --git a/lib/utils/dialogs.dart b/lib/utils/dialogs.dart index 84bb8202..8664705b 100644 --- a/lib/utils/dialogs.dart +++ b/lib/utils/dialogs.dart @@ -45,6 +45,61 @@ Future showConfirmDialog( return confirmed ?? false; } +/// Shows a confirmation dialog with an optional checkbox (e.g. "Don't ask again"). +/// Returns a record with [confirmed] and [checked] booleans. +Future<({bool confirmed, bool checked})> showConfirmDialogWithCheckbox( + BuildContext context, { + required String title, + required String message, + required String confirmText, + required String checkboxLabel, + String? cancelText, +}) async { + var checked = false; + final confirmed = await showDialog( + context: context, + builder: (dialogContext) { + return StatefulBuilder( + builder: (context, setDialogState) { + return AlertDialog( + title: Text(title), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(message), + const SizedBox(height: 12), + CheckboxListTile( + value: checked, + onChanged: (v) => setDialogState(() => checked = v ?? false), + title: Text(checkboxLabel), + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + dense: true, + ), + ], + ), + actions: [ + TextButton( + autofocus: true, + onPressed: () => Navigator.pop(dialogContext, false), + style: TextButton.styleFrom(padding: _buttonPadding, shape: _buttonShape), + child: Text(cancelText ?? t.common.cancel), + ), + FilledButton( + onPressed: () => Navigator.pop(dialogContext, true), + child: Text(confirmText), + ), + ], + ); + }, + ); + }, + ); + + return (confirmed: confirmed ?? false, checked: checked); +} + /// Shows a delete confirmation dialog. /// Convenience wrapper around [showConfirmDialog] with destructive styling. Future showDeleteConfirmation(BuildContext context, {required String title, required String message}) {