diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index 961c40a0..d8ebf686 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -51,7 +51,8 @@ "exitConfirmTitle": "App beenden?", "exitConfirmMessage": "Möchtest du die App wirklich beenden?", "dontAskAgain": "Nicht erneut fragen", - "exit": "Beenden" + "exit": "Beenden", + "viewAll": "Alle anzeigen" }, "screens": { "licenses": "Lizenzen", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 2c8ba036..5f010b4d 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -51,7 +51,8 @@ "exitConfirmTitle": "Exit app?", "exitConfirmMessage": "Are you sure you want to exit?", "dontAskAgain": "Don't ask again", - "exit": "Exit" + "exit": "Exit", + "viewAll": "View All" }, "screens": { "licenses": "Licenses", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index f8b8ff12..582ff382 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -51,7 +51,8 @@ "exitConfirmTitle": "¿Salir de la app?", "exitConfirmMessage": "¿Estás seguro de que quieres salir?", "dontAskAgain": "No volver a preguntar", - "exit": "Salir" + "exit": "Salir", + "viewAll": "Ver todo" }, "screens": { "licenses": "Licencias", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index f498893d..4fd1cc0b 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -51,7 +51,8 @@ "exitConfirmTitle": "Quitter l'application ?", "exitConfirmMessage": "Êtes-vous sûr de vouloir quitter ?", "dontAskAgain": "Ne plus demander", - "exit": "Quitter" + "exit": "Quitter", + "viewAll": "Tout afficher" }, "screens": { "licenses": "Licenses", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index bc15d832..31ab7150 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -51,7 +51,8 @@ "exitConfirmTitle": "Uscire dall'app?", "exitConfirmMessage": "Sei sicuro di voler uscire?", "dontAskAgain": "Non chiedere più", - "exit": "Esci" + "exit": "Esci", + "viewAll": "Mostra tutto" }, "screens": { "licenses": "Licenze", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 7e939131..794002cc 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -51,7 +51,8 @@ "exitConfirmTitle": "앱을 종료하시겠습니까?", "exitConfirmMessage": "정말 종료하시겠습니까?", "dontAskAgain": "다시 묻지 않기", - "exit": "종료" + "exit": "종료", + "viewAll": "모두 보기" }, "screens": { "licenses": "라이선스", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index 907e00ed..65b6f5a4 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -51,7 +51,8 @@ "exitConfirmTitle": "App afsluiten?", "exitConfirmMessage": "Weet je zeker dat je wilt afsluiten?", "dontAskAgain": "Niet meer vragen", - "exit": "Afsluiten" + "exit": "Afsluiten", + "viewAll": "Alles weergeven" }, "screens": { "licenses": "Licenties", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index cac362cc..c05ba857 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: 6669 (741 per locale) +/// Strings: 6687 (743 per locale) /// -/// Built on 2026-02-18 at 03:36 UTC +/// Built on 2026-02-20 at 20:23 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 043663c4..1ac628ea 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -149,6 +149,7 @@ class _TranslationsCommonDe implements TranslationsCommonEn { @override String get exitConfirmMessage => 'Möchtest du die App wirklich beenden?'; @override String get dontAskAgain => 'Nicht erneut fragen'; @override String get exit => 'Beenden'; + @override String get viewAll => 'Alle anzeigen'; } // Path: screens @@ -401,7 +402,6 @@ class _TranslationsMediaMenuDe implements TranslationsMediaMenuEn { @override String get mediaDeletedSuccessfully => 'Medienelement erfolgreich gelöscht'; @override String get mediaFailedToDelete => 'Löschen des Medienelements fehlgeschlagen'; @override String get rate => 'Bewerten'; - } // Path: accessibility @@ -1278,6 +1278,7 @@ extension on TranslationsDe { 'common.exitConfirmMessage' => 'Möchtest du die App wirklich beenden?', 'common.dontAskAgain' => 'Nicht erneut fragen', 'common.exit' => 'Beenden', + 'common.viewAll' => 'Alle anzeigen', 'screens.licenses' => 'Lizenzen', 'screens.switchProfile' => 'Profil wechseln', 'screens.subtitleStyling' => 'Untertitel-Stil', @@ -1476,7 +1477,6 @@ extension on TranslationsDe { 'mediaMenu.mediaDeletedSuccessfully' => 'Medienelement erfolgreich gelöscht', 'mediaMenu.mediaFailedToDelete' => 'Löschen des Medienelements fehlgeschlagen', 'mediaMenu.rate' => 'Bewerten', - 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, Film', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, Serie', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1742,10 +1742,10 @@ extension on TranslationsDe { 'liveTv.today' => 'Heute', 'liveTv.midnight' => 'Mitternacht', 'liveTv.overnight' => 'Nacht', - 'liveTv.morning' => 'Morgen', - 'liveTv.daytime' => 'Tagsüber', _ => null, } ?? switch (path) { + 'liveTv.morning' => 'Morgen', + 'liveTv.daytime' => 'Tagsüber', 'liveTv.evening' => 'Abend', 'liveTv.lateNight' => 'Spätnacht', 'liveTv.whatsOn' => 'Jetzt im TV', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index 9e6eae86..fb74a440 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -248,6 +248,9 @@ class TranslationsCommonEn { /// en: 'Exit' String get exit => 'Exit'; + + /// en: 'View All' + String get viewAll => 'View All'; } // Path: screens @@ -859,7 +862,6 @@ class TranslationsMediaMenuEn { /// en: 'Rate' String get rate => 'Rate'; - } // Path: accessibility @@ -2769,6 +2771,7 @@ extension on Translations { 'common.exitConfirmMessage' => 'Are you sure you want to exit?', 'common.dontAskAgain' => 'Don\'t ask again', 'common.exit' => 'Exit', + 'common.viewAll' => 'View All', 'screens.licenses' => 'Licenses', 'screens.switchProfile' => 'Switch Profile', 'screens.subtitleStyling' => 'Subtitle Styling', @@ -2967,7 +2970,6 @@ extension on Translations { 'mediaMenu.mediaDeletedSuccessfully' => 'Media item deleted successfully', 'mediaMenu.mediaFailedToDelete' => 'Failed to delete media item', 'mediaMenu.rate' => 'Rate', - 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, movie', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, TV show', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -3233,10 +3235,10 @@ extension on Translations { 'liveTv.today' => 'Today', 'liveTv.midnight' => 'Midnight', 'liveTv.overnight' => 'Overnight', - 'liveTv.morning' => 'Morning', - 'liveTv.daytime' => 'Daytime', _ => null, } ?? switch (path) { + 'liveTv.morning' => 'Morning', + 'liveTv.daytime' => 'Daytime', 'liveTv.evening' => 'Evening', 'liveTv.lateNight' => 'Late Night', 'liveTv.whatsOn' => 'What\'s On', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index 20e5bf89..5e618d15 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -149,6 +149,7 @@ class _TranslationsCommonEs implements TranslationsCommonEn { @override String get exitConfirmMessage => '¿Estás seguro de que quieres salir?'; @override String get dontAskAgain => 'No volver a preguntar'; @override String get exit => 'Salir'; + @override String get viewAll => 'Ver todo'; } // Path: screens @@ -401,7 +402,6 @@ class _TranslationsMediaMenuEs implements TranslationsMediaMenuEn { @override String get mediaDeletedSuccessfully => 'Elemento multimedia eliminado con éxito'; @override String get mediaFailedToDelete => 'Error al eliminar el elemento multimedia'; @override String get rate => 'Calificar'; - } // Path: accessibility @@ -1278,6 +1278,7 @@ extension on TranslationsEs { 'common.exitConfirmMessage' => '¿Estás seguro de que quieres salir?', 'common.dontAskAgain' => 'No volver a preguntar', 'common.exit' => 'Salir', + 'common.viewAll' => 'Ver todo', 'screens.licenses' => 'Licencias', 'screens.switchProfile' => 'Cambiar Perfil', 'screens.subtitleStyling' => 'Estilo de Subtítulos', @@ -1476,7 +1477,6 @@ extension on TranslationsEs { 'mediaMenu.mediaDeletedSuccessfully' => 'Elemento multimedia eliminado con éxito', 'mediaMenu.mediaFailedToDelete' => 'Error al eliminar el elemento multimedia', 'mediaMenu.rate' => 'Calificar', - 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, película', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, serie de TV', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1742,10 +1742,10 @@ extension on TranslationsEs { 'liveTv.today' => 'Hoy', 'liveTv.midnight' => 'Medianoche', 'liveTv.overnight' => 'Madrugada', - 'liveTv.morning' => 'Mañana', - 'liveTv.daytime' => 'Día', _ => null, } ?? switch (path) { + 'liveTv.morning' => 'Mañana', + 'liveTv.daytime' => 'Día', 'liveTv.evening' => 'Noche', 'liveTv.lateNight' => 'Trasnoche', 'liveTv.whatsOn' => 'En emisión', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index e123d0ef..a77a7ef3 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -149,6 +149,7 @@ class _TranslationsCommonFr implements TranslationsCommonEn { @override String get exitConfirmMessage => 'Êtes-vous sûr de vouloir quitter ?'; @override String get dontAskAgain => 'Ne plus demander'; @override String get exit => 'Quitter'; + @override String get viewAll => 'Tout afficher'; } // Path: screens @@ -401,7 +402,6 @@ class _TranslationsMediaMenuFr implements TranslationsMediaMenuEn { @override String get mediaDeletedSuccessfully => 'Élément média supprimé avec succès'; @override String get mediaFailedToDelete => 'Échec de la suppression de l\'élément média'; @override String get rate => 'Noter'; - } // Path: accessibility @@ -1278,6 +1278,7 @@ extension on TranslationsFr { 'common.exitConfirmMessage' => 'Êtes-vous sûr de vouloir quitter ?', 'common.dontAskAgain' => 'Ne plus demander', 'common.exit' => 'Quitter', + 'common.viewAll' => 'Tout afficher', 'screens.licenses' => 'Licenses', 'screens.switchProfile' => 'Changer de profil', 'screens.subtitleStyling' => 'Configuration des sous-titres', @@ -1476,7 +1477,6 @@ extension on TranslationsFr { 'mediaMenu.mediaDeletedSuccessfully' => 'Élément média supprimé avec succès', 'mediaMenu.mediaFailedToDelete' => 'Échec de la suppression de l\'élément média', 'mediaMenu.rate' => 'Noter', - 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, film', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, show TV', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1742,10 +1742,10 @@ extension on TranslationsFr { 'liveTv.today' => 'Aujourd\'hui', 'liveTv.midnight' => 'Minuit', 'liveTv.overnight' => 'Nuit', - 'liveTv.morning' => 'Matin', - 'liveTv.daytime' => 'Journée', _ => null, } ?? switch (path) { + 'liveTv.morning' => 'Matin', + 'liveTv.daytime' => 'Journée', 'liveTv.evening' => 'Soirée', 'liveTv.lateNight' => 'Nuit tardive', 'liveTv.whatsOn' => 'En ce moment', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index 75f8180b..2262c77f 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -149,6 +149,7 @@ class _TranslationsCommonIt implements TranslationsCommonEn { @override String get exitConfirmMessage => 'Sei sicuro di voler uscire?'; @override String get dontAskAgain => 'Non chiedere più'; @override String get exit => 'Esci'; + @override String get viewAll => 'Mostra tutto'; } // Path: screens @@ -401,7 +402,6 @@ class _TranslationsMediaMenuIt implements TranslationsMediaMenuEn { @override String get mediaDeletedSuccessfully => 'Elemento multimediale eliminato con successo'; @override String get mediaFailedToDelete => 'Impossibile eliminare l\'elemento multimediale'; @override String get rate => 'Valuta'; - } // Path: accessibility @@ -1278,6 +1278,7 @@ extension on TranslationsIt { 'common.exitConfirmMessage' => 'Sei sicuro di voler uscire?', 'common.dontAskAgain' => 'Non chiedere più', 'common.exit' => 'Esci', + 'common.viewAll' => 'Mostra tutto', 'screens.licenses' => 'Licenze', 'screens.switchProfile' => 'Cambia profilo', 'screens.subtitleStyling' => 'Stile sottotitoli', @@ -1476,7 +1477,6 @@ extension on TranslationsIt { 'mediaMenu.mediaDeletedSuccessfully' => 'Elemento multimediale eliminato con successo', 'mediaMenu.mediaFailedToDelete' => 'Impossibile eliminare l\'elemento multimediale', 'mediaMenu.rate' => 'Valuta', - 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, film', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, serie TV', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1742,10 +1742,10 @@ extension on TranslationsIt { 'liveTv.today' => 'Oggi', 'liveTv.midnight' => 'Mezzanotte', 'liveTv.overnight' => 'Notte', - 'liveTv.morning' => 'Mattina', - 'liveTv.daytime' => 'Giorno', _ => null, } ?? switch (path) { + 'liveTv.morning' => 'Mattina', + 'liveTv.daytime' => 'Giorno', 'liveTv.evening' => 'Sera', 'liveTv.lateNight' => 'Notte tarda', 'liveTv.whatsOn' => 'In onda ora', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index 33f89bcc..248c2f69 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -149,6 +149,7 @@ class _TranslationsCommonKo implements TranslationsCommonEn { @override String get exitConfirmMessage => '정말 종료하시겠습니까?'; @override String get dontAskAgain => '다시 묻지 않기'; @override String get exit => '종료'; + @override String get viewAll => '모두 보기'; } // Path: screens @@ -401,7 +402,6 @@ class _TranslationsMediaMenuKo implements TranslationsMediaMenuEn { @override String get mediaDeletedSuccessfully => '미디어 항목이 성공적으로 삭제되었습니다'; @override String get mediaFailedToDelete => '미디어 항목 삭제 실패'; @override String get rate => '평가'; - } // Path: accessibility @@ -1278,6 +1278,7 @@ extension on TranslationsKo { 'common.exitConfirmMessage' => '정말 종료하시겠습니까?', 'common.dontAskAgain' => '다시 묻지 않기', 'common.exit' => '종료', + 'common.viewAll' => '모두 보기', 'screens.licenses' => '라이선스', 'screens.switchProfile' => '프로필 전환', 'screens.subtitleStyling' => '자막 스타일 설정', @@ -1476,7 +1477,6 @@ extension on TranslationsKo { 'mediaMenu.mediaDeletedSuccessfully' => '미디어 항목이 성공적으로 삭제되었습니다', 'mediaMenu.mediaFailedToDelete' => '미디어 항목 삭제 실패', 'mediaMenu.rate' => '평가', - 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, 영화', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, TV 프로그램', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1742,10 +1742,10 @@ extension on TranslationsKo { 'liveTv.today' => '오늘', 'liveTv.midnight' => '자정', 'liveTv.overnight' => '심야', - 'liveTv.morning' => '아침', - 'liveTv.daytime' => '낮', _ => null, } ?? switch (path) { + 'liveTv.morning' => '아침', + 'liveTv.daytime' => '낮', 'liveTv.evening' => '저녁', 'liveTv.lateNight' => '심야 방송', 'liveTv.whatsOn' => '지금 방송 중', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index 8e16482d..3c970851 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -149,6 +149,7 @@ class _TranslationsCommonNl implements TranslationsCommonEn { @override String get exitConfirmMessage => 'Weet je zeker dat je wilt afsluiten?'; @override String get dontAskAgain => 'Niet meer vragen'; @override String get exit => 'Afsluiten'; + @override String get viewAll => 'Alles weergeven'; } // Path: screens @@ -401,7 +402,6 @@ class _TranslationsMediaMenuNl implements TranslationsMediaMenuEn { @override String get mediaDeletedSuccessfully => 'Media-item succesvol verwijderd'; @override String get mediaFailedToDelete => 'Verwijderen van media-item mislukt'; @override String get rate => 'Beoordelen'; - } // Path: accessibility @@ -1278,6 +1278,7 @@ extension on TranslationsNl { 'common.exitConfirmMessage' => 'Weet je zeker dat je wilt afsluiten?', 'common.dontAskAgain' => 'Niet meer vragen', 'common.exit' => 'Afsluiten', + 'common.viewAll' => 'Alles weergeven', 'screens.licenses' => 'Licenties', 'screens.switchProfile' => 'Wissel van profiel', 'screens.subtitleStyling' => 'Ondertitel opmaak', @@ -1476,7 +1477,6 @@ extension on TranslationsNl { 'mediaMenu.mediaDeletedSuccessfully' => 'Media-item succesvol verwijderd', 'mediaMenu.mediaFailedToDelete' => 'Verwijderen van media-item mislukt', 'mediaMenu.rate' => 'Beoordelen', - 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, film', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, TV-serie', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1742,10 +1742,10 @@ extension on TranslationsNl { 'liveTv.today' => 'Vandaag', 'liveTv.midnight' => 'Middernacht', 'liveTv.overnight' => 'Nacht', - 'liveTv.morning' => 'Ochtend', - 'liveTv.daytime' => 'Overdag', _ => null, } ?? switch (path) { + 'liveTv.morning' => 'Ochtend', + 'liveTv.daytime' => 'Overdag', 'liveTv.evening' => 'Avond', 'liveTv.lateNight' => 'Late avond', 'liveTv.whatsOn' => 'Nu op TV', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index 7e2fe4ce..f328733b 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -149,6 +149,7 @@ class _TranslationsCommonSv implements TranslationsCommonEn { @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'; + @override String get viewAll => 'Visa alla'; } // Path: screens @@ -401,7 +402,6 @@ class _TranslationsMediaMenuSv implements TranslationsMediaMenuEn { @override String get mediaDeletedSuccessfully => 'Mediaobjekt borttaget'; @override String get mediaFailedToDelete => 'Kunde inte ta bort mediaobjekt'; @override String get rate => 'Betygsätt'; - } // Path: accessibility @@ -1278,6 +1278,7 @@ extension on TranslationsSv { 'common.exitConfirmMessage' => 'Är du säker på att du vill avsluta?', 'common.dontAskAgain' => 'Fråga inte igen', 'common.exit' => 'Avsluta', + 'common.viewAll' => 'Visa alla', 'screens.licenses' => 'Licenser', 'screens.switchProfile' => 'Byt profil', 'screens.subtitleStyling' => 'Undertext-styling', @@ -1476,7 +1477,6 @@ extension on TranslationsSv { 'mediaMenu.mediaDeletedSuccessfully' => 'Mediaobjekt borttaget', 'mediaMenu.mediaFailedToDelete' => 'Kunde inte ta bort mediaobjekt', 'mediaMenu.rate' => 'Betygsätt', - 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, film', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, TV-serie', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1742,10 +1742,10 @@ extension on TranslationsSv { 'liveTv.today' => 'Idag', 'liveTv.midnight' => 'Midnatt', 'liveTv.overnight' => 'Natt', - 'liveTv.morning' => 'Morgon', - 'liveTv.daytime' => 'Dagtid', _ => null, } ?? switch (path) { + 'liveTv.morning' => 'Morgon', + 'liveTv.daytime' => 'Dagtid', 'liveTv.evening' => 'Kväll', 'liveTv.lateNight' => 'Sen kväll', 'liveTv.whatsOn' => 'På TV nu', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index 3e9c5a43..1969fc3b 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -149,6 +149,7 @@ class _TranslationsCommonZh implements TranslationsCommonEn { @override String get exitConfirmMessage => '确定要退出吗?'; @override String get dontAskAgain => '不再询问'; @override String get exit => '退出'; + @override String get viewAll => '查看全部'; } // Path: screens @@ -401,7 +402,6 @@ class _TranslationsMediaMenuZh implements TranslationsMediaMenuEn { @override String get mediaDeletedSuccessfully => '媒体项已成功删除'; @override String get mediaFailedToDelete => '删除媒体项失败'; @override String get rate => '评分'; - } // Path: accessibility @@ -1278,6 +1278,7 @@ extension on TranslationsZh { 'common.exitConfirmMessage' => '确定要退出吗?', 'common.dontAskAgain' => '不再询问', 'common.exit' => '退出', + 'common.viewAll' => '查看全部', 'screens.licenses' => '许可证', 'screens.switchProfile' => '切换用户', 'screens.subtitleStyling' => '字幕样式', @@ -1476,7 +1477,6 @@ extension on TranslationsZh { 'mediaMenu.mediaDeletedSuccessfully' => '媒体项已成功删除', 'mediaMenu.mediaFailedToDelete' => '删除媒体项失败', 'mediaMenu.rate' => '评分', - 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, 电影', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, 电视剧', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1742,10 +1742,10 @@ extension on TranslationsZh { 'liveTv.today' => '今天', 'liveTv.midnight' => '午夜', 'liveTv.overnight' => '凌晨', - 'liveTv.morning' => '上午', - 'liveTv.daytime' => '白天', _ => null, } ?? switch (path) { + 'liveTv.morning' => '上午', + 'liveTv.daytime' => '白天', 'liveTv.evening' => '晚上', 'liveTv.lateNight' => '深夜', 'liveTv.whatsOn' => '正在播出', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index bed00149..36ff8d09 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -51,7 +51,8 @@ "exitConfirmTitle": "Avsluta appen?", "exitConfirmMessage": "Är du säker på att du vill avsluta?", "dontAskAgain": "Fråga inte igen", - "exit": "Avsluta" + "exit": "Avsluta", + "viewAll": "Visa alla" }, "screens": { "licenses": "Licenser", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index 9171c579..cfb81c41 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -51,7 +51,8 @@ "exitConfirmTitle": "退出应用?", "exitConfirmMessage": "确定要退出吗?", "dontAskAgain": "不再询问", - "exit": "退出" + "exit": "退出", + "viewAll": "查看全部" }, "screens": { "licenses": "许可证", diff --git a/lib/screens/hub_detail_screen.dart b/lib/screens/hub_detail_screen.dart index fbeab7e3..01e22afb 100644 --- a/lib/screens/hub_detail_screen.dart +++ b/lib/screens/hub_detail_screen.dart @@ -10,9 +10,17 @@ import '../providers/settings_provider.dart'; import '../services/settings_service.dart'; import '../utils/provider_extensions.dart'; import '../utils/app_logger.dart'; -import '../widgets/media_grid_sliver.dart'; -import '../widgets/focused_scroll_scaffold.dart'; +import '../utils/grid_size_calculator.dart'; +import '../widgets/focusable_media_card.dart'; +import '../widgets/media_grid_delegate.dart'; +import '../widgets/desktop_app_bar.dart'; import '../widgets/overlay_sheet.dart'; +import 'package:flutter/services.dart'; +import '../focus/dpad_navigator.dart'; +import '../focus/focus_theme.dart'; +import '../focus/input_mode_tracker.dart'; +import '../focus/key_event_utils.dart'; +import '../mixins/grid_focus_node_mixin.dart'; import 'libraries/sort_bottom_sheet.dart'; import 'libraries/state_messages.dart'; import '../mixins/refreshable.dart'; @@ -28,7 +36,7 @@ class HubDetailScreen extends StatefulWidget { State createState() => _HubDetailScreenState(); } -class _HubDetailScreenState extends State with Refreshable { +class _HubDetailScreenState extends State with Refreshable, GridFocusNodeMixin { PlexClient get client => _getClientForHub(); List _items = []; @@ -39,6 +47,14 @@ class _HubDetailScreenState extends State with Refreshable { bool _isLoading = false; String? _errorMessage; + late final FocusNode _firstItemFocusNode = FocusNode(debugLabel: 'hub_detail_first_item'); + late final FocusNode _sortButtonFocusNode = FocusNode(debugLabel: 'hub_detail_sort'); + bool _isAppBarFocused = false; + bool _backHandledByKeyEvent = false; + + /// Key for getting a context below OverlaySheetHost + final GlobalKey _overlayChildKey = GlobalKey(); + /// Get the correct PlexClient for this hub's server PlexClient _getClientForHub() { return context.getClientForServer(widget.hub.serverId!); @@ -47,6 +63,7 @@ class _HubDetailScreenState extends State with Refreshable { @override void initState() { super.initState(); + _sortButtonFocusNode.addListener(_onSortButtonFocusChange); // Start with items already loaded in the hub _items = widget.hub.items; _filteredItems = widget.hub.items; @@ -56,6 +73,72 @@ class _HubDetailScreenState extends State with Refreshable { } // Load sorts based on the library type _loadSorts(); + // Auto-focus first grid item in keyboard mode after first frame + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + if (InputModeTracker.isKeyboardMode(context) && _filteredItems.isNotEmpty) { + _firstItemFocusNode.requestFocus(); + } + }); + } + + @override + void dispose() { + _sortButtonFocusNode.removeListener(_onSortButtonFocusChange); + _firstItemFocusNode.dispose(); + _sortButtonFocusNode.dispose(); + disposeGridFocusNodes(); + super.dispose(); + } + + void _onSortButtonFocusChange() { + if (!mounted) return; + final hasFocus = _sortButtonFocusNode.hasFocus; + if (hasFocus && !_isAppBarFocused) { + setState(() => _isAppBarFocused = true); + } else if (!hasFocus && _isAppBarFocused) { + setState(() => _isAppBarFocused = false); + } + } + + void _focusGrid() { + if (_filteredItems.isEmpty) return; + final targetIndex = + shouldRestoreGridFocus && lastFocusedGridIndex! < _filteredItems.length ? lastFocusedGridIndex! : 0; + if (targetIndex == 0) { + _firstItemFocusNode.requestFocus(); + } else { + getGridItemFocusNode(targetIndex, prefix: 'hub_detail_item').requestFocus(); + } + } + + void _navigateToAppBar() { + setState(() => _isAppBarFocused = true); + _sortButtonFocusNode.requestFocus(); + } + + void _handleBackFromContent() { + _backHandledByKeyEvent = true; + _navigateToAppBar(); + } + + KeyEventResult _handleSortButtonKeyEvent(FocusNode _, KeyEvent event) { + final key = event.logicalKey; + + final backResult = handleBackKeyAction(event, () => Navigator.pop(context)); + if (backResult != KeyEventResult.ignored) return backResult; + + if (event is! KeyDownEvent) return KeyEventResult.ignored; + + if (key.isDownKey) { + _focusGrid(); + return KeyEventResult.handled; + } + if (key.isSelectKey) { + _showSortBottomSheet(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; } Future _loadSorts() async { @@ -153,7 +236,8 @@ class _HubDetailScreenState extends State with Refreshable { } void _showSortBottomSheet() { - OverlaySheetController.of(context).show( + final overlayContext = _overlayChildKey.currentContext ?? context; + OverlaySheetController.of(overlayContext).show( builder: (context) => SortBottomSheet( sortOptions: _sortOptions, selectedSort: _selectedSort, @@ -230,58 +314,126 @@ class _HubDetailScreenState extends State with Refreshable { @override Widget build(BuildContext context) { - return OverlaySheetHost( - child: FocusedScrollScaffold( - title: Text(widget.hub.title), - actions: [ - IconButton( - icon: AppIcon(Symbols.swap_vert_rounded, fill: 1, semanticLabel: t.libraries.sort), - onPressed: _showSortBottomSheet, - ), - ], - slivers: [ - if (_errorMessage != null) - SliverFillRemaining( - child: ErrorStateWidget( - message: _errorMessage!, - icon: Symbols.error_outline_rounded, - onRetry: _loadMoreItems, + final isKeyboardMode = InputModeTracker.isKeyboardMode(context); + final sortButtonFocused = isKeyboardMode && _isAppBarFocused; + + return PopScope( + canPop: !isKeyboardMode || _isAppBarFocused, + onPopInvokedWithResult: (didPop, _) { + if (didPop || _backHandledByKeyEvent) { + _backHandledByKeyEvent = false; + return; + } + _navigateToAppBar(); + }, + child: OverlaySheetHost( + child: Scaffold( + key: _overlayChildKey, + body: CustomScrollView( + clipBehavior: Clip.none, + slivers: [ + CustomAppBar( + title: Text(widget.hub.title), + pinned: true, + actions: [ + Focus( + focusNode: _sortButtonFocusNode, + onKeyEvent: _handleSortButtonKeyEvent, + child: Container( + decoration: FocusTheme.focusBackgroundDecoration(isFocused: sortButtonFocused, borderRadius: 20), + child: IconButton( + icon: AppIcon(Symbols.swap_vert_rounded, fill: 1, semanticLabel: t.libraries.sort), + onPressed: _showSortBottomSheet, + ), + ), + ), + ], ), - ) - else if (_filteredItems.isEmpty && _isLoading) - const SliverFillRemaining(child: Center(child: CircularProgressIndicator())) - else if (_filteredItems.isEmpty) - SliverFillRemaining(child: Center(child: Text(t.hubDetail.noItemsFound))) - else - Builder( - builder: (context) { - final episodePosterMode = context.watch().episodePosterMode; + if (_errorMessage != null) + SliverFillRemaining( + child: ErrorStateWidget( + message: _errorMessage!, + icon: Symbols.error_outline_rounded, + onRetry: _loadMoreItems, + ), + ) + else if (_filteredItems.isEmpty && _isLoading) + const SliverFillRemaining(child: Center(child: CircularProgressIndicator())) + else if (_filteredItems.isEmpty) + SliverFillRemaining(child: Center(child: Text(t.hubDetail.noItemsFound))) + else + Builder( + builder: (context) { + final settings = context.watch(); + final episodePosterMode = settings.episodePosterMode; - // Determine hub content type for layout decisions - final hasEpisodes = _filteredItems.any((item) => item.usesWideAspectRatio(episodePosterMode)); - final hasNonEpisodes = _filteredItems.any((item) => !item.usesWideAspectRatio(episodePosterMode)); + // Determine hub content type for layout decisions + final hasEpisodes = _filteredItems.any((item) => item.usesWideAspectRatio(episodePosterMode)); + final hasNonEpisodes = _filteredItems.any((item) => !item.usesWideAspectRatio(episodePosterMode)); - // Mixed hub = has both episodes AND non-episodes - final isMixedHub = hasEpisodes && hasNonEpisodes; + // Mixed hub = has both episodes AND non-episodes + final isMixedHub = hasEpisodes && hasNonEpisodes; - // Episode-only = all items are episodes with thumbnails - final isEpisodeOnlyHub = hasEpisodes && !hasNonEpisodes; + // Episode-only = all items are episodes with thumbnails + final isEpisodeOnlyHub = hasEpisodes && !hasNonEpisodes; - // Use 16:9 for episode-only hubs OR mixed hubs (with episode thumbnail mode) - final useWideLayout = - episodePosterMode == EpisodePosterMode.episodeThumbnail && (isEpisodeOnlyHub || isMixedHub); + // Use 16:9 for episode-only hubs OR mixed hubs (with episode thumbnail mode) + final useWideLayout = + episodePosterMode == EpisodePosterMode.episodeThumbnail && (isEpisodeOnlyHub || isMixedHub); - return MediaGridSliver( - items: _filteredItems, - onRefresh: _handleItemRefresh, - usePaddingAwareExtent: true, - horizontalPadding: 16, - useWideAspectRatio: useWideLayout, - mixedHubContext: isMixedHub, - ); - }, - ), - ], + return SliverPadding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + sliver: SliverLayoutBuilder( + builder: (context, constraints) { + final maxExtent = GridSizeCalculator.getMaxCrossAxisExtentWithPadding( + context, + settings.libraryDensity, + 16, + ); + final columnCount = GridSizeCalculator.getColumnCount( + constraints.crossAxisExtent, + useWideLayout ? maxExtent * 1.8 : maxExtent, + ); + + return SliverGrid( + gridDelegate: MediaGridDelegate.createDelegate( + context: context, + density: settings.libraryDensity, + usePaddingAware: true, + horizontalPadding: 16, + useWideAspectRatio: useWideLayout, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + final item = _filteredItems[index]; + final focusNode = index == 0 + ? _firstItemFocusNode + : getGridItemFocusNode(index, prefix: 'hub_detail_item'); + final isFirstRow = GridSizeCalculator.isFirstRow(index, columnCount); + final isFirstColumn = GridSizeCalculator.isFirstColumn(index, columnCount); + + return FocusableMediaCard( + focusNode: focusNode, + item: item, + onRefresh: _handleItemRefresh, + onNavigateUp: isFirstRow ? _navigateToAppBar : null, + onNavigateLeft: isFirstColumn ? () {} : null, + onBack: _handleBackFromContent, + onFocusChange: (hasFocus) => trackGridItemFocus(index, hasFocus), + mixedHubContext: isMixedHub, + ); + }, + childCount: _filteredItems.length, + ), + ); + }, + ), + ); + }, + ), + ], + ), + ), ), ); } diff --git a/lib/widgets/focusable_media_card.dart b/lib/widgets/focusable_media_card.dart index 17328742..dc213bfe 100644 --- a/lib/widgets/focusable_media_card.dart +++ b/lib/widgets/focusable_media_card.dart @@ -24,6 +24,9 @@ class FocusableMediaCard extends StatefulWidget { /// True for downloaded content without server access final bool isOffline; + /// True when in a hub with mixed content (movies + episodes) + final bool mixedHubContext; + /// Optional external focus node for programmatic focus control. /// If not provided, an internal focus node is created. final FocusNode? focusNode; @@ -60,6 +63,7 @@ class FocusableMediaCard extends StatefulWidget { this.isInContinueWatching = false, this.collectionId, this.isOffline = false, + this.mixedHubContext = false, this.focusNode, this.onNavigateUp, this.onNavigateLeft, @@ -102,6 +106,7 @@ class _FocusableMediaCardState extends State { isInContinueWatching: widget.isInContinueWatching, collectionId: widget.collectionId, isOffline: widget.isOffline, + mixedHubContext: widget.mixedHubContext, ), ); } diff --git a/lib/widgets/hub_section.dart b/lib/widgets/hub_section.dart index aa678cc4..2ad14bc1 100644 --- a/lib/widgets/hub_section.dart +++ b/lib/widgets/hub_section.dart @@ -6,6 +6,7 @@ import 'package:plezy/widgets/app_icon.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import '../focus/dpad_navigator.dart'; +import '../focus/input_mode_tracker.dart'; import '../focus/key_event_utils.dart'; import '../providers/settings_provider.dart'; import '../services/settings_service.dart' show EpisodePosterMode, LibraryDensity; @@ -94,12 +95,15 @@ class HubSectionState extends State { _hubFocusNode.addListener(_onFocusChange); } + /// Total item count including the optional "View All" card + int get _totalItemCount => widget.hub.items.length + (widget.hub.more ? 1 : 0); + @override void didUpdateWidget(HubSection oldWidget) { super.didUpdateWidget(oldWidget); // Clamp focus index if item count changed if (widget.hub.items.length != oldWidget.hub.items.length) { - final maxIndex = widget.hub.items.isEmpty ? 0 : widget.hub.items.length - 1; + final maxIndex = _totalItemCount == 0 ? 0 : _totalItemCount - 1; if (_focusedIndex > maxIndex) { _focusedIndex = maxIndex; } @@ -128,9 +132,9 @@ class HubSectionState extends State { /// Request focus on this hub at a specific item index void requestFocusAt(int index) { - if (widget.hub.items.isEmpty) return; + if (_totalItemCount == 0) return; - final clamped = index.clamp(0, widget.hub.items.length - 1); + final clamped = index.clamp(0, _totalItemCount - 1); _focusedIndex = clamped; // Remember this position for this specific hub HubFocusMemory.setForHub(widget.hub.hubKey, clamped); @@ -145,7 +149,7 @@ class HubSectionState extends State { /// Request focus using the stored memory for this hub void requestFocusFromMemory() { - final index = HubFocusMemory.getForHub(widget.hub.hubKey, widget.hub.items.length); + final index = HubFocusMemory.getForHub(widget.hub.hubKey, _totalItemCount); requestFocusAt(index); } @@ -166,7 +170,7 @@ class HubSectionState extends State { bool get hasFocusedItem => _hubFocusNode.hasFocus; /// Get the number of items in this hub - int get itemCount => widget.hub.items.length; + int get itemCount => _totalItemCount; /// Scroll to center the item at the given index void _scrollToIndex(int index, {bool animate = true}) { @@ -225,8 +229,8 @@ class HubSectionState extends State { return KeyEventResult.ignored; } - final itemCount = widget.hub.items.length; - if (itemCount == 0) return KeyEventResult.ignored; + final totalCount = _totalItemCount; + if (totalCount == 0) return KeyEventResult.ignored; // Left: move to previous item, or navigate to sidebar at left edge if (key.isLeftKey) { @@ -246,7 +250,7 @@ class HubSectionState extends State { // Right: move to next item, ALWAYS consume to prevent escape if (key.isRightKey) { - if (_focusedIndex < itemCount - 1) { + if (_focusedIndex < totalCount - 1) { setState(() { _focusedIndex++; }); @@ -287,12 +291,18 @@ class HubSectionState extends State { } void _activateCurrentItem() { + if (_focusedIndex == widget.hub.items.length && widget.hub.more) { + _navigateToHubDetail(context); + return; + } if (_focusedIndex >= widget.hub.items.length) return; final item = widget.hub.items[_focusedIndex]; _navigateToItem(item); } void _showContextMenuForCurrentItem() { + // No context menu for the "View All" card + if (_focusedIndex >= widget.hub.items.length) return; _mediaCardKeys[_focusedIndex]?.currentState?.showContextMenu(); } @@ -307,6 +317,7 @@ class HubSectionState extends State { @override Widget build(BuildContext context) { final hasFocus = _hubFocusNode.hasFocus; + final isKeyboardMode = InputModeTracker.isKeyboardMode(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -350,7 +361,7 @@ class HubSectionState extends State { ), ), ], - if (widget.hub.more) ...[ + if (widget.hub.more && !isKeyboardMode) ...[ const SizedBox(width: 4), const AppIcon(Symbols.chevron_right_rounded, fill: 1, size: 20), ], @@ -425,11 +436,49 @@ class HubSectionState extends State { controller: scrollController, scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), - itemCount: widget.hub.items.length, + itemCount: isKeyboardMode ? _totalItemCount : widget.hub.items.length, itemBuilder: (context, index) { - final item = widget.hub.items[index]; final isItemFocused = hasFocus && index == _focusedIndex; + // "View All" card at end + if (index == widget.hub.items.length) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: _LockedHubItemWrapper( + isFocused: isItemFocused, + onTap: () { + _onItemTapped(index); + _navigateToHubDetail(context); + }, + child: SizedBox( + width: 80, + height: containerHeight - 10, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Symbols.arrow_forward_rounded, + size: 32, + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7), + ), + const SizedBox(height: 4), + Text( + t.common.viewAll, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + ], + ), + ), + ), + ), + ); + } + + final item = widget.hub.items[index]; + return Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: _LockedHubItemWrapper(