@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -51,7 +51,8 @@
|
||||
"exitConfirmTitle": "앱을 종료하시겠습니까?",
|
||||
"exitConfirmMessage": "정말 종료하시겠습니까?",
|
||||
"dontAskAgain": "다시 묻지 않기",
|
||||
"exit": "종료"
|
||||
"exit": "종료",
|
||||
"viewAll": "모두 보기"
|
||||
},
|
||||
"screens": {
|
||||
"licenses": "라이선스",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' => '지금 방송 중',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' => '正在播出',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -51,7 +51,8 @@
|
||||
"exitConfirmTitle": "退出应用?",
|
||||
"exitConfirmMessage": "确定要退出吗?",
|
||||
"dontAskAgain": "不再询问",
|
||||
"exit": "退出"
|
||||
"exit": "退出",
|
||||
"viewAll": "查看全部"
|
||||
},
|
||||
"screens": {
|
||||
"licenses": "许可证",
|
||||
|
||||
@@ -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<HubDetailScreen> createState() => _HubDetailScreenState();
|
||||
}
|
||||
|
||||
class _HubDetailScreenState extends State<HubDetailScreen> with Refreshable {
|
||||
class _HubDetailScreenState extends State<HubDetailScreen> with Refreshable, GridFocusNodeMixin {
|
||||
PlexClient get client => _getClientForHub();
|
||||
|
||||
List<PlexMetadata> _items = [];
|
||||
@@ -39,6 +47,14 @@ class _HubDetailScreenState extends State<HubDetailScreen> 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<HubDetailScreen> 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<HubDetailScreen> 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<void> _loadSorts() async {
|
||||
@@ -153,7 +236,8 @@ class _HubDetailScreenState extends State<HubDetailScreen> 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<HubDetailScreen> 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<SettingsProvider>().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<SettingsProvider>();
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<FocusableMediaCard> {
|
||||
isInContinueWatching: widget.isInContinueWatching,
|
||||
collectionId: widget.collectionId,
|
||||
isOffline: widget.isOffline,
|
||||
mixedHubContext: widget.mixedHubContext,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<HubSection> {
|
||||
_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<HubSection> {
|
||||
|
||||
/// 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<HubSection> {
|
||||
|
||||
/// 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<HubSection> {
|
||||
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<HubSection> {
|
||||
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<HubSection> {
|
||||
|
||||
// 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<HubSection> {
|
||||
}
|
||||
|
||||
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<HubSection> {
|
||||
@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<HubSection> {
|
||||
),
|
||||
),
|
||||
],
|
||||
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<HubSection> {
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user