feat(media): lazily page detail episodes

close #1245
This commit is contained in:
edde746
2026-06-05 19:26:02 +02:00
parent e6ca5f254d
commit 0fd0506551
46 changed files with 1428 additions and 501 deletions
+2
View File
@@ -508,8 +508,10 @@
"metadataRefreshFailed": "Неуспешно опресняване на метаданни: ${error}", "metadataRefreshFailed": "Неуспешно опресняване на метаданни: ${error}",
"logoutConfirm": "Сигурни ли сте, че искате да излезете?", "logoutConfirm": "Сигурни ли сте, че искате да излезете?",
"noSeasonsFound": "Не са намерени сезони", "noSeasonsFound": "Не са намерени сезони",
"seasonsLoadFailed": "Неуспешно зареждане на сезони",
"noEpisodesFound": "Не са намерени епизоди в първия сезон", "noEpisodesFound": "Не са намерени епизоди в първия сезон",
"noEpisodesFoundGeneral": "Не са намерени епизоди", "noEpisodesFoundGeneral": "Не са намерени епизоди",
"episodesLoadFailed": "Неуспешно зареждане на епизоди",
"noResultsFound": "Няма намерени резултати", "noResultsFound": "Няма намерени резултати",
"sleepTimerSet": "Таймерът за заспиване е зададен за ${label}", "sleepTimerSet": "Таймерът за заспиване е зададен за ${label}",
"noItemsAvailable": "Няма налични елементи", "noItemsAvailable": "Няма налични елементи",
+2
View File
@@ -508,8 +508,10 @@
"metadataRefreshFailed": "Kunne ikke opdatere metadata: ${error}", "metadataRefreshFailed": "Kunne ikke opdatere metadata: ${error}",
"logoutConfirm": "Er du sikker på, at du vil logge ud?", "logoutConfirm": "Er du sikker på, at du vil logge ud?",
"noSeasonsFound": "Ingen sæsoner fundet", "noSeasonsFound": "Ingen sæsoner fundet",
"seasonsLoadFailed": "Kunne ikke indlæse sæsoner",
"noEpisodesFound": "Ingen episoder fundet i første sæson", "noEpisodesFound": "Ingen episoder fundet i første sæson",
"noEpisodesFoundGeneral": "Ingen episoder fundet", "noEpisodesFoundGeneral": "Ingen episoder fundet",
"episodesLoadFailed": "Kunne ikke indlæse episoder",
"noResultsFound": "Ingen resultater fundet", "noResultsFound": "Ingen resultater fundet",
"sleepTimerSet": "Sove-timer indstillet til ${label}", "sleepTimerSet": "Sove-timer indstillet til ${label}",
"noItemsAvailable": "Ingen elementer tilgængelige", "noItemsAvailable": "Ingen elementer tilgængelige",
+2
View File
@@ -508,8 +508,10 @@
"metadataRefreshFailed": "Metadaten konnten nicht aktualisiert werden: ${error}", "metadataRefreshFailed": "Metadaten konnten nicht aktualisiert werden: ${error}",
"logoutConfirm": "Abmeldung wirklich durchführen?", "logoutConfirm": "Abmeldung wirklich durchführen?",
"noSeasonsFound": "Keine Staffeln gefunden", "noSeasonsFound": "Keine Staffeln gefunden",
"seasonsLoadFailed": "Staffeln konnten nicht geladen werden",
"noEpisodesFound": "Keine Episoden in der ersten Staffel gefunden", "noEpisodesFound": "Keine Episoden in der ersten Staffel gefunden",
"noEpisodesFoundGeneral": "Keine Episoden gefunden", "noEpisodesFoundGeneral": "Keine Episoden gefunden",
"episodesLoadFailed": "Episoden konnten nicht geladen werden",
"noResultsFound": "Keine Ergebnisse gefunden", "noResultsFound": "Keine Ergebnisse gefunden",
"sleepTimerSet": "Sleep-Timer gesetzt auf ${label}", "sleepTimerSet": "Sleep-Timer gesetzt auf ${label}",
"noItemsAvailable": "Keine Elemente verfügbar", "noItemsAvailable": "Keine Elemente verfügbar",
+2
View File
@@ -508,8 +508,10 @@
"metadataRefreshFailed": "Failed to refresh metadata: ${error}", "metadataRefreshFailed": "Failed to refresh metadata: ${error}",
"logoutConfirm": "Are you sure you want to logout?", "logoutConfirm": "Are you sure you want to logout?",
"noSeasonsFound": "No seasons found", "noSeasonsFound": "No seasons found",
"seasonsLoadFailed": "Couldn't load seasons",
"noEpisodesFound": "No episodes found in first season", "noEpisodesFound": "No episodes found in first season",
"noEpisodesFoundGeneral": "No episodes found", "noEpisodesFoundGeneral": "No episodes found",
"episodesLoadFailed": "Couldn't load episodes",
"noResultsFound": "No results found", "noResultsFound": "No results found",
"sleepTimerSet": "Sleep timer set for ${label}", "sleepTimerSet": "Sleep timer set for ${label}",
"noItemsAvailable": "No items available", "noItemsAvailable": "No items available",
+2
View File
@@ -508,8 +508,10 @@
"metadataRefreshFailed": "Error al actualizar metadatos: ${error}", "metadataRefreshFailed": "Error al actualizar metadatos: ${error}",
"logoutConfirm": "¿Estás seguro de que quieres cerrar sesión?", "logoutConfirm": "¿Estás seguro de que quieres cerrar sesión?",
"noSeasonsFound": "No se encontraron temporadas", "noSeasonsFound": "No se encontraron temporadas",
"seasonsLoadFailed": "No se pudieron cargar las temporadas",
"noEpisodesFound": "No se encontraron episodios en la primera temporada", "noEpisodesFound": "No se encontraron episodios en la primera temporada",
"noEpisodesFoundGeneral": "No se encontraron episodios", "noEpisodesFoundGeneral": "No se encontraron episodios",
"episodesLoadFailed": "No se pudieron cargar los episodios",
"noResultsFound": "No se encontraron resultados", "noResultsFound": "No se encontraron resultados",
"sleepTimerSet": "Temporizador establecido en ${label}", "sleepTimerSet": "Temporizador establecido en ${label}",
"noItemsAvailable": "No hay elementos disponibles", "noItemsAvailable": "No hay elementos disponibles",
+2
View File
@@ -508,8 +508,10 @@
"metadataRefreshFailed": "Échec de l'actualisation des métadonnées: ${error}", "metadataRefreshFailed": "Échec de l'actualisation des métadonnées: ${error}",
"logoutConfirm": "Êtes-vous sûr de vouloir vous déconnecter ?", "logoutConfirm": "Êtes-vous sûr de vouloir vous déconnecter ?",
"noSeasonsFound": "Aucune saison trouvée", "noSeasonsFound": "Aucune saison trouvée",
"seasonsLoadFailed": "Impossible de charger les saisons",
"noEpisodesFound": "Aucun épisode trouvé dans la première saison", "noEpisodesFound": "Aucun épisode trouvé dans la première saison",
"noEpisodesFoundGeneral": "Aucun épisode trouvé", "noEpisodesFoundGeneral": "Aucun épisode trouvé",
"episodesLoadFailed": "Impossible de charger les épisodes",
"noResultsFound": "Aucun résultat trouvé", "noResultsFound": "Aucun résultat trouvé",
"sleepTimerSet": "Minuterie de mise en veille réglée sur ${label}", "sleepTimerSet": "Minuterie de mise en veille réglée sur ${label}",
"noItemsAvailable": "Aucun élément disponible", "noItemsAvailable": "Aucun élément disponible",
+2
View File
@@ -508,8 +508,10 @@
"metadataRefreshFailed": "Errore aggiornamento metadati: ${error}", "metadataRefreshFailed": "Errore aggiornamento metadati: ${error}",
"logoutConfirm": "Sei sicuro di volerti disconnettere?", "logoutConfirm": "Sei sicuro di volerti disconnettere?",
"noSeasonsFound": "Nessuna stagione trovata", "noSeasonsFound": "Nessuna stagione trovata",
"seasonsLoadFailed": "Impossibile caricare le stagioni",
"noEpisodesFound": "Nessun episodio trovato nella prima stagione", "noEpisodesFound": "Nessun episodio trovato nella prima stagione",
"noEpisodesFoundGeneral": "Nessun episodio trovato", "noEpisodesFoundGeneral": "Nessun episodio trovato",
"episodesLoadFailed": "Impossibile caricare gli episodi",
"noResultsFound": "Nessun risultato", "noResultsFound": "Nessun risultato",
"sleepTimerSet": "Imposta timer spegnimento per ${label}", "sleepTimerSet": "Imposta timer spegnimento per ${label}",
"noItemsAvailable": "Nessun elemento disponibile", "noItemsAvailable": "Nessun elemento disponibile",
+2
View File
@@ -508,8 +508,10 @@
"metadataRefreshFailed": "メタデータの更新に失敗しました: ${error}", "metadataRefreshFailed": "メタデータの更新に失敗しました: ${error}",
"logoutConfirm": "ログアウトしてもよろしいですか?", "logoutConfirm": "ログアウトしてもよろしいですか?",
"noSeasonsFound": "シーズンが見つかりません", "noSeasonsFound": "シーズンが見つかりません",
"seasonsLoadFailed": "シーズンを読み込めませんでした",
"noEpisodesFound": "最初のシーズンにエピソードが見つかりません", "noEpisodesFound": "最初のシーズンにエピソードが見つかりません",
"noEpisodesFoundGeneral": "エピソードが見つかりません", "noEpisodesFoundGeneral": "エピソードが見つかりません",
"episodesLoadFailed": "エピソードを読み込めませんでした",
"noResultsFound": "結果が見つかりません", "noResultsFound": "結果が見つかりません",
"sleepTimerSet": "スリープタイマーを${label}に設定しました", "sleepTimerSet": "スリープタイマーを${label}に設定しました",
"noItemsAvailable": "アイテムがありません", "noItemsAvailable": "アイテムがありません",
+2
View File
@@ -508,8 +508,10 @@
"metadataRefreshFailed": "메타데이터 새로고침 실패: ${error}", "metadataRefreshFailed": "메타데이터 새로고침 실패: ${error}",
"logoutConfirm": "로그아웃 하시겠습니까?", "logoutConfirm": "로그아웃 하시겠습니까?",
"noSeasonsFound": "시즌을 찾을 수 없음", "noSeasonsFound": "시즌을 찾을 수 없음",
"seasonsLoadFailed": "시즌을 불러오지 못했습니다",
"noEpisodesFound": "시즌 1에서 에피소드를 찾을 수 없습니다", "noEpisodesFound": "시즌 1에서 에피소드를 찾을 수 없습니다",
"noEpisodesFoundGeneral": "에피소드를 찾을 수 없습니다", "noEpisodesFoundGeneral": "에피소드를 찾을 수 없습니다",
"episodesLoadFailed": "에피소드를 불러오지 못했습니다",
"noResultsFound": "결과를 찾을 수 없습니다", "noResultsFound": "결과를 찾을 수 없습니다",
"sleepTimerSet": "수면 타이머가 ${label}로 설정 되었습니다", "sleepTimerSet": "수면 타이머가 ${label}로 설정 되었습니다",
"noItemsAvailable": "사용 가능한 항목이 없습니다", "noItemsAvailable": "사용 가능한 항목이 없습니다",
+2
View File
@@ -508,8 +508,10 @@
"metadataRefreshFailed": "Kunne ikke oppdatere metadata: ${error}", "metadataRefreshFailed": "Kunne ikke oppdatere metadata: ${error}",
"logoutConfirm": "Er du sikker på at du vil logge ut?", "logoutConfirm": "Er du sikker på at du vil logge ut?",
"noSeasonsFound": "Ingen sesonger funnet", "noSeasonsFound": "Ingen sesonger funnet",
"seasonsLoadFailed": "Kunne ikke laste sesonger",
"noEpisodesFound": "Ingen episoder funnet i første sesong", "noEpisodesFound": "Ingen episoder funnet i første sesong",
"noEpisodesFoundGeneral": "Ingen episoder funnet", "noEpisodesFoundGeneral": "Ingen episoder funnet",
"episodesLoadFailed": "Kunne ikke laste episoder",
"noResultsFound": "Ingen resultater funnet", "noResultsFound": "Ingen resultater funnet",
"sleepTimerSet": "Søvntimer satt til ${label}", "sleepTimerSet": "Søvntimer satt til ${label}",
"noItemsAvailable": "Ingen elementer tilgjengelig", "noItemsAvailable": "Ingen elementer tilgjengelig",
+2
View File
@@ -508,8 +508,10 @@
"metadataRefreshFailed": "Kon metadata niet vernieuwen: ${error}", "metadataRefreshFailed": "Kon metadata niet vernieuwen: ${error}",
"logoutConfirm": "Weet je zeker dat je wilt uitloggen?", "logoutConfirm": "Weet je zeker dat je wilt uitloggen?",
"noSeasonsFound": "Geen seizoenen gevonden", "noSeasonsFound": "Geen seizoenen gevonden",
"seasonsLoadFailed": "Kan seizoenen niet laden",
"noEpisodesFound": "Geen afleveringen gevonden in eerste seizoen", "noEpisodesFound": "Geen afleveringen gevonden in eerste seizoen",
"noEpisodesFoundGeneral": "Geen afleveringen gevonden", "noEpisodesFoundGeneral": "Geen afleveringen gevonden",
"episodesLoadFailed": "Kan afleveringen niet laden",
"noResultsFound": "Geen resultaten gevonden", "noResultsFound": "Geen resultaten gevonden",
"sleepTimerSet": "Slaap timer ingesteld voor ${label}", "sleepTimerSet": "Slaap timer ingesteld voor ${label}",
"noItemsAvailable": "Geen items beschikbaar", "noItemsAvailable": "Geen items beschikbaar",
+2
View File
@@ -508,8 +508,10 @@
"metadataRefreshFailed": "Nie udało się odświeżyć metadanych: ${error}", "metadataRefreshFailed": "Nie udało się odświeżyć metadanych: ${error}",
"logoutConfirm": "Czy na pewno chcesz się wylogować?", "logoutConfirm": "Czy na pewno chcesz się wylogować?",
"noSeasonsFound": "Nie znaleziono sezonów", "noSeasonsFound": "Nie znaleziono sezonów",
"seasonsLoadFailed": "Nie udało się załadować sezonów",
"noEpisodesFound": "Nie znaleziono odcinków w pierwszym sezonie", "noEpisodesFound": "Nie znaleziono odcinków w pierwszym sezonie",
"noEpisodesFoundGeneral": "Nie znaleziono odcinków", "noEpisodesFoundGeneral": "Nie znaleziono odcinków",
"episodesLoadFailed": "Nie udało się załadować odcinków",
"noResultsFound": "Nie znaleziono wyników", "noResultsFound": "Nie znaleziono wyników",
"sleepTimerSet": "Wyłącznik czasowy ustawiony na ${label}", "sleepTimerSet": "Wyłącznik czasowy ustawiony na ${label}",
"noItemsAvailable": "Brak dostępnych elementów", "noItemsAvailable": "Brak dostępnych elementów",
+2
View File
@@ -508,8 +508,10 @@
"metadataRefreshFailed": "Falha ao atualizar metadados: ${error}", "metadataRefreshFailed": "Falha ao atualizar metadados: ${error}",
"logoutConfirm": "Tem certeza que deseja sair?", "logoutConfirm": "Tem certeza que deseja sair?",
"noSeasonsFound": "Nenhuma temporada encontrada", "noSeasonsFound": "Nenhuma temporada encontrada",
"seasonsLoadFailed": "Não foi possível carregar as temporadas",
"noEpisodesFound": "Nenhum episódio encontrado na primeira temporada", "noEpisodesFound": "Nenhum episódio encontrado na primeira temporada",
"noEpisodesFoundGeneral": "Nenhum episódio encontrado", "noEpisodesFoundGeneral": "Nenhum episódio encontrado",
"episodesLoadFailed": "Não foi possível carregar os episódios",
"noResultsFound": "Nenhum resultado encontrado", "noResultsFound": "Nenhum resultado encontrado",
"sleepTimerSet": "Timer de sono definido para ${label}", "sleepTimerSet": "Timer de sono definido para ${label}",
"noItemsAvailable": "Nenhum item disponível", "noItemsAvailable": "Nenhum item disponível",
+2
View File
@@ -508,8 +508,10 @@
"metadataRefreshFailed": "Не удалось обновить метаданные: ${error}", "metadataRefreshFailed": "Не удалось обновить метаданные: ${error}",
"logoutConfirm": "Вы уверены, что хотите выйти?", "logoutConfirm": "Вы уверены, что хотите выйти?",
"noSeasonsFound": "Сезоны не найдены", "noSeasonsFound": "Сезоны не найдены",
"seasonsLoadFailed": "Не удалось загрузить сезоны",
"noEpisodesFound": "Эпизоды в первом сезоне не найдены", "noEpisodesFound": "Эпизоды в первом сезоне не найдены",
"noEpisodesFoundGeneral": "Эпизоды не найдены", "noEpisodesFoundGeneral": "Эпизоды не найдены",
"episodesLoadFailed": "Не удалось загрузить эпизоды",
"noResultsFound": "Результаты не найдены", "noResultsFound": "Результаты не найдены",
"sleepTimerSet": "Таймер сна установлен на ${label}", "sleepTimerSet": "Таймер сна установлен на ${label}",
"noItemsAvailable": "Нет доступных элементов", "noItemsAvailable": "Нет доступных элементов",
+1 -1
View File
@@ -4,7 +4,7 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 16 /// Locales: 16
/// Strings: 19072 (1192 per locale) /// Strings: 19104 (1194 per locale)
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint, unused_import // ignore_for_file: type=lint, unused_import
+8 -4
View File
@@ -672,8 +672,10 @@ class _TranslationsMessagesBg extends TranslationsMessagesEn {
@override String metadataRefreshFailed({required Object error}) => 'Неуспешно опресняване на метаданни: ${error}'; @override String metadataRefreshFailed({required Object error}) => 'Неуспешно опресняване на метаданни: ${error}';
@override String get logoutConfirm => 'Сигурни ли сте, че искате да излезете?'; @override String get logoutConfirm => 'Сигурни ли сте, че искате да излезете?';
@override String get noSeasonsFound => 'Не са намерени сезони'; @override String get noSeasonsFound => 'Не са намерени сезони';
@override String get seasonsLoadFailed => 'Неуспешно зареждане на сезони';
@override String get noEpisodesFound => 'Не са намерени епизоди в първия сезон'; @override String get noEpisodesFound => 'Не са намерени епизоди в първия сезон';
@override String get noEpisodesFoundGeneral => 'Не са намерени епизоди'; @override String get noEpisodesFoundGeneral => 'Не са намерени епизоди';
@override String get episodesLoadFailed => 'Неуспешно зареждане на епизоди';
@override String get noResultsFound => 'Няма намерени резултати'; @override String get noResultsFound => 'Няма намерени резултати';
@override String sleepTimerSet({required Object label}) => 'Таймерът за заспиване е зададен за ${label}'; @override String sleepTimerSet({required Object label}) => 'Таймерът за заспиване е зададен за ${label}';
@override String get noItemsAvailable => 'Няма налични елементи'; @override String get noItemsAvailable => 'Няма налични елементи';
@@ -2295,8 +2297,10 @@ extension on TranslationsBg {
'messages.metadataRefreshFailed' => ({required Object error}) => 'Неуспешно опресняване на метаданни: ${error}', 'messages.metadataRefreshFailed' => ({required Object error}) => 'Неуспешно опресняване на метаданни: ${error}',
'messages.logoutConfirm' => 'Сигурни ли сте, че искате да излезете?', 'messages.logoutConfirm' => 'Сигурни ли сте, че искате да излезете?',
'messages.noSeasonsFound' => 'Не са намерени сезони', 'messages.noSeasonsFound' => 'Не са намерени сезони',
'messages.seasonsLoadFailed' => 'Неуспешно зареждане на сезони',
'messages.noEpisodesFound' => 'Не са намерени епизоди в първия сезон', 'messages.noEpisodesFound' => 'Не са намерени епизоди в първия сезон',
'messages.noEpisodesFoundGeneral' => 'Не са намерени епизоди', 'messages.noEpisodesFoundGeneral' => 'Не са намерени епизоди',
'messages.episodesLoadFailed' => 'Неуспешно зареждане на епизоди',
'messages.noResultsFound' => 'Няма намерени резултати', 'messages.noResultsFound' => 'Няма намерени резултати',
'messages.sleepTimerSet' => ({required Object label}) => 'Таймерът за заспиване е зададен за ${label}', 'messages.sleepTimerSet' => ({required Object label}) => 'Таймерът за заспиване е зададен за ${label}',
'messages.noItemsAvailable' => 'Няма налични елементи', 'messages.noItemsAvailable' => 'Няма налични елементи',
@@ -2331,10 +2335,10 @@ extension on TranslationsBg {
'mpvConfig.loadPreset' => 'Зареди', 'mpvConfig.loadPreset' => 'Зареди',
'mpvConfig.deletePreset' => 'Изтрий', 'mpvConfig.deletePreset' => 'Изтрий',
'mpvConfig.presetSaved' => 'Пресетът е запазен', 'mpvConfig.presetSaved' => 'Пресетът е запазен',
'mpvConfig.presetLoaded' => 'Пресетът е зареден',
'mpvConfig.presetDeleted' => 'Пресетът е изтрит',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'mpvConfig.presetLoaded' => 'Пресетът е зареден',
'mpvConfig.presetDeleted' => 'Пресетът е изтрит',
'mpvConfig.confirmDeletePreset' => 'Сигурни ли сте, че искате да изтриете този пресет?', 'mpvConfig.confirmDeletePreset' => 'Сигурни ли сте, че искате да изтриете този пресет?',
'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment',
'dialog.confirmAction' => 'Потвърждение на действие', 'dialog.confirmAction' => 'Потвърждение на действие',
@@ -2845,10 +2849,10 @@ extension on TranslationsBg {
'externalPlayer.playerPackage' => 'Име на пакет', 'externalPlayer.playerPackage' => 'Име на пакет',
'externalPlayer.playerUrlScheme' => 'URL схема', 'externalPlayer.playerUrlScheme' => 'URL схема',
'externalPlayer.off' => 'Изключено', 'externalPlayer.off' => 'Изключено',
'externalPlayer.launchFailed' => 'Неуспешно отваряне на външен плеър',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} не е инсталиран',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'externalPlayer.launchFailed' => 'Неуспешно отваряне на външен плеър',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} не е инсталиран',
'externalPlayer.playInExternalPlayer' => 'Пусни във външен плеър', 'externalPlayer.playInExternalPlayer' => 'Пусни във външен плеър',
'metadataEdit.editMetadata' => 'Редактирай...', 'metadataEdit.editMetadata' => 'Редактирай...',
'metadataEdit.screenTitle' => 'Редактиране на метаданни', 'metadataEdit.screenTitle' => 'Редактиране на метаданни',
+8 -4
View File
@@ -672,8 +672,10 @@ class _TranslationsMessagesDa extends TranslationsMessagesEn {
@override String metadataRefreshFailed({required Object error}) => 'Kunne ikke opdatere metadata: ${error}'; @override String metadataRefreshFailed({required Object error}) => 'Kunne ikke opdatere metadata: ${error}';
@override String get logoutConfirm => 'Er du sikker på, at du vil logge ud?'; @override String get logoutConfirm => 'Er du sikker på, at du vil logge ud?';
@override String get noSeasonsFound => 'Ingen sæsoner fundet'; @override String get noSeasonsFound => 'Ingen sæsoner fundet';
@override String get seasonsLoadFailed => 'Kunne ikke indlæse sæsoner';
@override String get noEpisodesFound => 'Ingen episoder fundet i første sæson'; @override String get noEpisodesFound => 'Ingen episoder fundet i første sæson';
@override String get noEpisodesFoundGeneral => 'Ingen episoder fundet'; @override String get noEpisodesFoundGeneral => 'Ingen episoder fundet';
@override String get episodesLoadFailed => 'Kunne ikke indlæse episoder';
@override String get noResultsFound => 'Ingen resultater fundet'; @override String get noResultsFound => 'Ingen resultater fundet';
@override String sleepTimerSet({required Object label}) => 'Sove-timer indstillet til ${label}'; @override String sleepTimerSet({required Object label}) => 'Sove-timer indstillet til ${label}';
@override String get noItemsAvailable => 'Ingen elementer tilgængelige'; @override String get noItemsAvailable => 'Ingen elementer tilgængelige';
@@ -2295,8 +2297,10 @@ extension on TranslationsDa {
'messages.metadataRefreshFailed' => ({required Object error}) => 'Kunne ikke opdatere metadata: ${error}', 'messages.metadataRefreshFailed' => ({required Object error}) => 'Kunne ikke opdatere metadata: ${error}',
'messages.logoutConfirm' => 'Er du sikker på, at du vil logge ud?', 'messages.logoutConfirm' => 'Er du sikker på, at du vil logge ud?',
'messages.noSeasonsFound' => 'Ingen sæsoner fundet', 'messages.noSeasonsFound' => 'Ingen sæsoner fundet',
'messages.seasonsLoadFailed' => 'Kunne ikke indlæse sæsoner',
'messages.noEpisodesFound' => 'Ingen episoder fundet i første sæson', 'messages.noEpisodesFound' => 'Ingen episoder fundet i første sæson',
'messages.noEpisodesFoundGeneral' => 'Ingen episoder fundet', 'messages.noEpisodesFoundGeneral' => 'Ingen episoder fundet',
'messages.episodesLoadFailed' => 'Kunne ikke indlæse episoder',
'messages.noResultsFound' => 'Ingen resultater fundet', 'messages.noResultsFound' => 'Ingen resultater fundet',
'messages.sleepTimerSet' => ({required Object label}) => 'Sove-timer indstillet til ${label}', 'messages.sleepTimerSet' => ({required Object label}) => 'Sove-timer indstillet til ${label}',
'messages.noItemsAvailable' => 'Ingen elementer tilgængelige', 'messages.noItemsAvailable' => 'Ingen elementer tilgængelige',
@@ -2331,10 +2335,10 @@ extension on TranslationsDa {
'mpvConfig.loadPreset' => 'Indlæs', 'mpvConfig.loadPreset' => 'Indlæs',
'mpvConfig.deletePreset' => 'Slet', 'mpvConfig.deletePreset' => 'Slet',
'mpvConfig.presetSaved' => 'Forudindstilling gemt', 'mpvConfig.presetSaved' => 'Forudindstilling gemt',
'mpvConfig.presetLoaded' => 'Forudindstilling indlæst',
'mpvConfig.presetDeleted' => 'Forudindstilling slettet',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'mpvConfig.presetLoaded' => 'Forudindstilling indlæst',
'mpvConfig.presetDeleted' => 'Forudindstilling slettet',
'mpvConfig.confirmDeletePreset' => 'Er du sikker på, at du vil slette denne forudindstilling?', 'mpvConfig.confirmDeletePreset' => 'Er du sikker på, at du vil slette denne forudindstilling?',
'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment',
'dialog.confirmAction' => 'Bekræft handling', 'dialog.confirmAction' => 'Bekræft handling',
@@ -2845,10 +2849,10 @@ extension on TranslationsDa {
'externalPlayer.playerPackage' => 'Pakkenavn', 'externalPlayer.playerPackage' => 'Pakkenavn',
'externalPlayer.playerUrlScheme' => 'URL-skema', 'externalPlayer.playerUrlScheme' => 'URL-skema',
'externalPlayer.off' => 'Fra', 'externalPlayer.off' => 'Fra',
'externalPlayer.launchFailed' => 'Kunne ikke åbne ekstern afspiller',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} er ikke installeret',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'externalPlayer.launchFailed' => 'Kunne ikke åbne ekstern afspiller',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} er ikke installeret',
'externalPlayer.playInExternalPlayer' => 'Afspil i ekstern afspiller', 'externalPlayer.playInExternalPlayer' => 'Afspil i ekstern afspiller',
'metadataEdit.editMetadata' => 'Redigér...', 'metadataEdit.editMetadata' => 'Redigér...',
'metadataEdit.screenTitle' => 'Redigér metadata', 'metadataEdit.screenTitle' => 'Redigér metadata',
+8 -4
View File
@@ -672,8 +672,10 @@ class _TranslationsMessagesDe extends TranslationsMessagesEn {
@override String metadataRefreshFailed({required Object error}) => 'Metadaten konnten nicht aktualisiert werden: ${error}'; @override String metadataRefreshFailed({required Object error}) => 'Metadaten konnten nicht aktualisiert werden: ${error}';
@override String get logoutConfirm => 'Abmeldung wirklich durchführen?'; @override String get logoutConfirm => 'Abmeldung wirklich durchführen?';
@override String get noSeasonsFound => 'Keine Staffeln gefunden'; @override String get noSeasonsFound => 'Keine Staffeln gefunden';
@override String get seasonsLoadFailed => 'Staffeln konnten nicht geladen werden';
@override String get noEpisodesFound => 'Keine Episoden in der ersten Staffel gefunden'; @override String get noEpisodesFound => 'Keine Episoden in der ersten Staffel gefunden';
@override String get noEpisodesFoundGeneral => 'Keine Episoden gefunden'; @override String get noEpisodesFoundGeneral => 'Keine Episoden gefunden';
@override String get episodesLoadFailed => 'Episoden konnten nicht geladen werden';
@override String get noResultsFound => 'Keine Ergebnisse gefunden'; @override String get noResultsFound => 'Keine Ergebnisse gefunden';
@override String sleepTimerSet({required Object label}) => 'Sleep-Timer gesetzt auf ${label}'; @override String sleepTimerSet({required Object label}) => 'Sleep-Timer gesetzt auf ${label}';
@override String get noItemsAvailable => 'Keine Elemente verfügbar'; @override String get noItemsAvailable => 'Keine Elemente verfügbar';
@@ -2295,8 +2297,10 @@ extension on TranslationsDe {
'messages.metadataRefreshFailed' => ({required Object error}) => 'Metadaten konnten nicht aktualisiert werden: ${error}', 'messages.metadataRefreshFailed' => ({required Object error}) => 'Metadaten konnten nicht aktualisiert werden: ${error}',
'messages.logoutConfirm' => 'Abmeldung wirklich durchführen?', 'messages.logoutConfirm' => 'Abmeldung wirklich durchführen?',
'messages.noSeasonsFound' => 'Keine Staffeln gefunden', 'messages.noSeasonsFound' => 'Keine Staffeln gefunden',
'messages.seasonsLoadFailed' => 'Staffeln konnten nicht geladen werden',
'messages.noEpisodesFound' => 'Keine Episoden in der ersten Staffel gefunden', 'messages.noEpisodesFound' => 'Keine Episoden in der ersten Staffel gefunden',
'messages.noEpisodesFoundGeneral' => 'Keine Episoden gefunden', 'messages.noEpisodesFoundGeneral' => 'Keine Episoden gefunden',
'messages.episodesLoadFailed' => 'Episoden konnten nicht geladen werden',
'messages.noResultsFound' => 'Keine Ergebnisse gefunden', 'messages.noResultsFound' => 'Keine Ergebnisse gefunden',
'messages.sleepTimerSet' => ({required Object label}) => 'Sleep-Timer gesetzt auf ${label}', 'messages.sleepTimerSet' => ({required Object label}) => 'Sleep-Timer gesetzt auf ${label}',
'messages.noItemsAvailable' => 'Keine Elemente verfügbar', 'messages.noItemsAvailable' => 'Keine Elemente verfügbar',
@@ -2331,10 +2335,10 @@ extension on TranslationsDe {
'mpvConfig.loadPreset' => 'Laden', 'mpvConfig.loadPreset' => 'Laden',
'mpvConfig.deletePreset' => 'Löschen', 'mpvConfig.deletePreset' => 'Löschen',
'mpvConfig.presetSaved' => 'Voreinstellung gespeichert', 'mpvConfig.presetSaved' => 'Voreinstellung gespeichert',
'mpvConfig.presetLoaded' => 'Voreinstellung geladen',
'mpvConfig.presetDeleted' => 'Voreinstellung gelöscht',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'mpvConfig.presetLoaded' => 'Voreinstellung geladen',
'mpvConfig.presetDeleted' => 'Voreinstellung gelöscht',
'mpvConfig.confirmDeletePreset' => 'Möchten Sie diese Voreinstellung wirklich löschen?', 'mpvConfig.confirmDeletePreset' => 'Möchten Sie diese Voreinstellung wirklich löschen?',
'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment',
'dialog.confirmAction' => 'Aktion bestätigen', 'dialog.confirmAction' => 'Aktion bestätigen',
@@ -2845,10 +2849,10 @@ extension on TranslationsDe {
'externalPlayer.playerPackage' => 'Paketname', 'externalPlayer.playerPackage' => 'Paketname',
'externalPlayer.playerUrlScheme' => 'URL-Schema', 'externalPlayer.playerUrlScheme' => 'URL-Schema',
'externalPlayer.off' => 'Aus', 'externalPlayer.off' => 'Aus',
'externalPlayer.launchFailed' => 'Externer Player konnte nicht geöffnet werden',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} ist nicht installiert',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'externalPlayer.launchFailed' => 'Externer Player konnte nicht geöffnet werden',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} ist nicht installiert',
'externalPlayer.playInExternalPlayer' => 'In externem Player abspielen', 'externalPlayer.playInExternalPlayer' => 'In externem Player abspielen',
'metadataEdit.editMetadata' => 'Bearbeiten...', 'metadataEdit.editMetadata' => 'Bearbeiten...',
'metadataEdit.screenTitle' => 'Metadaten bearbeiten', 'metadataEdit.screenTitle' => 'Metadaten bearbeiten',
+12 -4
View File
@@ -1560,12 +1560,18 @@ class TranslationsMessagesEn {
/// en: 'No seasons found' /// en: 'No seasons found'
String get noSeasonsFound => 'No seasons found'; String get noSeasonsFound => 'No seasons found';
/// en: 'Couldn't load seasons'
String get seasonsLoadFailed => 'Couldn\'t load seasons';
/// en: 'No episodes found in first season' /// en: 'No episodes found in first season'
String get noEpisodesFound => 'No episodes found in first season'; String get noEpisodesFound => 'No episodes found in first season';
/// en: 'No episodes found' /// en: 'No episodes found'
String get noEpisodesFoundGeneral => 'No episodes found'; String get noEpisodesFoundGeneral => 'No episodes found';
/// en: 'Couldn't load episodes'
String get episodesLoadFailed => 'Couldn\'t load episodes';
/// en: 'No results found' /// en: 'No results found'
String get noResultsFound => 'No results found'; String get noResultsFound => 'No results found';
@@ -4685,8 +4691,10 @@ extension on Translations {
'messages.metadataRefreshFailed' => ({required Object error}) => 'Failed to refresh metadata: ${error}', 'messages.metadataRefreshFailed' => ({required Object error}) => 'Failed to refresh metadata: ${error}',
'messages.logoutConfirm' => 'Are you sure you want to logout?', 'messages.logoutConfirm' => 'Are you sure you want to logout?',
'messages.noSeasonsFound' => 'No seasons found', 'messages.noSeasonsFound' => 'No seasons found',
'messages.seasonsLoadFailed' => 'Couldn\'t load seasons',
'messages.noEpisodesFound' => 'No episodes found in first season', 'messages.noEpisodesFound' => 'No episodes found in first season',
'messages.noEpisodesFoundGeneral' => 'No episodes found', 'messages.noEpisodesFoundGeneral' => 'No episodes found',
'messages.episodesLoadFailed' => 'Couldn\'t load episodes',
'messages.noResultsFound' => 'No results found', 'messages.noResultsFound' => 'No results found',
'messages.sleepTimerSet' => ({required Object label}) => 'Sleep timer set for ${label}', 'messages.sleepTimerSet' => ({required Object label}) => 'Sleep timer set for ${label}',
'messages.noItemsAvailable' => 'No items available', 'messages.noItemsAvailable' => 'No items available',
@@ -4721,10 +4729,10 @@ extension on Translations {
'mpvConfig.loadPreset' => 'Load', 'mpvConfig.loadPreset' => 'Load',
'mpvConfig.deletePreset' => 'Delete', 'mpvConfig.deletePreset' => 'Delete',
'mpvConfig.presetSaved' => 'Preset saved', 'mpvConfig.presetSaved' => 'Preset saved',
'mpvConfig.presetLoaded' => 'Preset loaded',
'mpvConfig.presetDeleted' => 'Preset deleted',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'mpvConfig.presetLoaded' => 'Preset loaded',
'mpvConfig.presetDeleted' => 'Preset deleted',
'mpvConfig.confirmDeletePreset' => 'Are you sure you want to delete this preset?', 'mpvConfig.confirmDeletePreset' => 'Are you sure you want to delete this preset?',
'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment',
'dialog.confirmAction' => 'Confirm Action', 'dialog.confirmAction' => 'Confirm Action',
@@ -5235,10 +5243,10 @@ extension on Translations {
'externalPlayer.playerPackage' => 'Package Name', 'externalPlayer.playerPackage' => 'Package Name',
'externalPlayer.playerUrlScheme' => 'URL Scheme', 'externalPlayer.playerUrlScheme' => 'URL Scheme',
'externalPlayer.off' => 'Off', 'externalPlayer.off' => 'Off',
'externalPlayer.launchFailed' => 'Failed to open external player',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} is not installed',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'externalPlayer.launchFailed' => 'Failed to open external player',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} is not installed',
'externalPlayer.playInExternalPlayer' => 'Play in External Player', 'externalPlayer.playInExternalPlayer' => 'Play in External Player',
'metadataEdit.editMetadata' => 'Edit...', 'metadataEdit.editMetadata' => 'Edit...',
'metadataEdit.screenTitle' => 'Edit Metadata', 'metadataEdit.screenTitle' => 'Edit Metadata',
+8 -4
View File
@@ -672,8 +672,10 @@ class _TranslationsMessagesEs extends TranslationsMessagesEn {
@override String metadataRefreshFailed({required Object error}) => 'Error al actualizar metadatos: ${error}'; @override String metadataRefreshFailed({required Object error}) => 'Error al actualizar metadatos: ${error}';
@override String get logoutConfirm => '¿Estás seguro de que quieres cerrar sesión?'; @override String get logoutConfirm => '¿Estás seguro de que quieres cerrar sesión?';
@override String get noSeasonsFound => 'No se encontraron temporadas'; @override String get noSeasonsFound => 'No se encontraron temporadas';
@override String get seasonsLoadFailed => 'No se pudieron cargar las temporadas';
@override String get noEpisodesFound => 'No se encontraron episodios en la primera temporada'; @override String get noEpisodesFound => 'No se encontraron episodios en la primera temporada';
@override String get noEpisodesFoundGeneral => 'No se encontraron episodios'; @override String get noEpisodesFoundGeneral => 'No se encontraron episodios';
@override String get episodesLoadFailed => 'No se pudieron cargar los episodios';
@override String get noResultsFound => 'No se encontraron resultados'; @override String get noResultsFound => 'No se encontraron resultados';
@override String sleepTimerSet({required Object label}) => 'Temporizador establecido en ${label}'; @override String sleepTimerSet({required Object label}) => 'Temporizador establecido en ${label}';
@override String get noItemsAvailable => 'No hay elementos disponibles'; @override String get noItemsAvailable => 'No hay elementos disponibles';
@@ -2295,8 +2297,10 @@ extension on TranslationsEs {
'messages.metadataRefreshFailed' => ({required Object error}) => 'Error al actualizar metadatos: ${error}', 'messages.metadataRefreshFailed' => ({required Object error}) => 'Error al actualizar metadatos: ${error}',
'messages.logoutConfirm' => '¿Estás seguro de que quieres cerrar sesión?', 'messages.logoutConfirm' => '¿Estás seguro de que quieres cerrar sesión?',
'messages.noSeasonsFound' => 'No se encontraron temporadas', 'messages.noSeasonsFound' => 'No se encontraron temporadas',
'messages.seasonsLoadFailed' => 'No se pudieron cargar las temporadas',
'messages.noEpisodesFound' => 'No se encontraron episodios en la primera temporada', 'messages.noEpisodesFound' => 'No se encontraron episodios en la primera temporada',
'messages.noEpisodesFoundGeneral' => 'No se encontraron episodios', 'messages.noEpisodesFoundGeneral' => 'No se encontraron episodios',
'messages.episodesLoadFailed' => 'No se pudieron cargar los episodios',
'messages.noResultsFound' => 'No se encontraron resultados', 'messages.noResultsFound' => 'No se encontraron resultados',
'messages.sleepTimerSet' => ({required Object label}) => 'Temporizador establecido en ${label}', 'messages.sleepTimerSet' => ({required Object label}) => 'Temporizador establecido en ${label}',
'messages.noItemsAvailable' => 'No hay elementos disponibles', 'messages.noItemsAvailable' => 'No hay elementos disponibles',
@@ -2331,10 +2335,10 @@ extension on TranslationsEs {
'mpvConfig.loadPreset' => 'Cargar', 'mpvConfig.loadPreset' => 'Cargar',
'mpvConfig.deletePreset' => 'Eliminar', 'mpvConfig.deletePreset' => 'Eliminar',
'mpvConfig.presetSaved' => 'Ajuste guardado', 'mpvConfig.presetSaved' => 'Ajuste guardado',
'mpvConfig.presetLoaded' => 'Ajuste cargado',
'mpvConfig.presetDeleted' => 'Ajuste eliminado',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'mpvConfig.presetLoaded' => 'Ajuste cargado',
'mpvConfig.presetDeleted' => 'Ajuste eliminado',
'mpvConfig.confirmDeletePreset' => '¿Estás seguro de que quieres eliminar este ajuste?', 'mpvConfig.confirmDeletePreset' => '¿Estás seguro de que quieres eliminar este ajuste?',
'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment',
'dialog.confirmAction' => 'Confirmar Acción', 'dialog.confirmAction' => 'Confirmar Acción',
@@ -2845,10 +2849,10 @@ extension on TranslationsEs {
'externalPlayer.playerPackage' => 'Nombre del paquete', 'externalPlayer.playerPackage' => 'Nombre del paquete',
'externalPlayer.playerUrlScheme' => 'Esquema URL', 'externalPlayer.playerUrlScheme' => 'Esquema URL',
'externalPlayer.off' => 'Desactivado', 'externalPlayer.off' => 'Desactivado',
'externalPlayer.launchFailed' => 'No se pudo abrir el reproductor externo',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} no está instalado',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'externalPlayer.launchFailed' => 'No se pudo abrir el reproductor externo',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} no está instalado',
'externalPlayer.playInExternalPlayer' => 'Reproducir en reproductor externo', 'externalPlayer.playInExternalPlayer' => 'Reproducir en reproductor externo',
'metadataEdit.editMetadata' => 'Editar...', 'metadataEdit.editMetadata' => 'Editar...',
'metadataEdit.screenTitle' => 'Editar metadatos', 'metadataEdit.screenTitle' => 'Editar metadatos',
+8 -4
View File
@@ -672,8 +672,10 @@ class _TranslationsMessagesFr extends TranslationsMessagesEn {
@override String metadataRefreshFailed({required Object error}) => 'Échec de l\'actualisation des métadonnées: ${error}'; @override String metadataRefreshFailed({required Object error}) => 'Échec de l\'actualisation des métadonnées: ${error}';
@override String get logoutConfirm => 'Êtes-vous sûr de vouloir vous déconnecter ?'; @override String get logoutConfirm => 'Êtes-vous sûr de vouloir vous déconnecter ?';
@override String get noSeasonsFound => 'Aucune saison trouvée'; @override String get noSeasonsFound => 'Aucune saison trouvée';
@override String get seasonsLoadFailed => 'Impossible de charger les saisons';
@override String get noEpisodesFound => 'Aucun épisode trouvé dans la première saison'; @override String get noEpisodesFound => 'Aucun épisode trouvé dans la première saison';
@override String get noEpisodesFoundGeneral => 'Aucun épisode trouvé'; @override String get noEpisodesFoundGeneral => 'Aucun épisode trouvé';
@override String get episodesLoadFailed => 'Impossible de charger les épisodes';
@override String get noResultsFound => 'Aucun résultat trouvé'; @override String get noResultsFound => 'Aucun résultat trouvé';
@override String sleepTimerSet({required Object label}) => 'Minuterie de mise en veille réglée sur ${label}'; @override String sleepTimerSet({required Object label}) => 'Minuterie de mise en veille réglée sur ${label}';
@override String get noItemsAvailable => 'Aucun élément disponible'; @override String get noItemsAvailable => 'Aucun élément disponible';
@@ -2295,8 +2297,10 @@ extension on TranslationsFr {
'messages.metadataRefreshFailed' => ({required Object error}) => 'Échec de l\'actualisation des métadonnées: ${error}', 'messages.metadataRefreshFailed' => ({required Object error}) => 'Échec de l\'actualisation des métadonnées: ${error}',
'messages.logoutConfirm' => 'Êtes-vous sûr de vouloir vous déconnecter ?', 'messages.logoutConfirm' => 'Êtes-vous sûr de vouloir vous déconnecter ?',
'messages.noSeasonsFound' => 'Aucune saison trouvée', 'messages.noSeasonsFound' => 'Aucune saison trouvée',
'messages.seasonsLoadFailed' => 'Impossible de charger les saisons',
'messages.noEpisodesFound' => 'Aucun épisode trouvé dans la première saison', 'messages.noEpisodesFound' => 'Aucun épisode trouvé dans la première saison',
'messages.noEpisodesFoundGeneral' => 'Aucun épisode trouvé', 'messages.noEpisodesFoundGeneral' => 'Aucun épisode trouvé',
'messages.episodesLoadFailed' => 'Impossible de charger les épisodes',
'messages.noResultsFound' => 'Aucun résultat trouvé', 'messages.noResultsFound' => 'Aucun résultat trouvé',
'messages.sleepTimerSet' => ({required Object label}) => 'Minuterie de mise en veille réglée sur ${label}', 'messages.sleepTimerSet' => ({required Object label}) => 'Minuterie de mise en veille réglée sur ${label}',
'messages.noItemsAvailable' => 'Aucun élément disponible', 'messages.noItemsAvailable' => 'Aucun élément disponible',
@@ -2331,10 +2335,10 @@ extension on TranslationsFr {
'mpvConfig.loadPreset' => 'Charger', 'mpvConfig.loadPreset' => 'Charger',
'mpvConfig.deletePreset' => 'Supprimer', 'mpvConfig.deletePreset' => 'Supprimer',
'mpvConfig.presetSaved' => 'Préréglage enregistré', 'mpvConfig.presetSaved' => 'Préréglage enregistré',
'mpvConfig.presetLoaded' => 'Préréglage chargé',
'mpvConfig.presetDeleted' => 'Préréglage supprimé',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'mpvConfig.presetLoaded' => 'Préréglage chargé',
'mpvConfig.presetDeleted' => 'Préréglage supprimé',
'mpvConfig.confirmDeletePreset' => 'Êtes-vous sûr de vouloir supprimer ce préréglage ?', 'mpvConfig.confirmDeletePreset' => 'Êtes-vous sûr de vouloir supprimer ce préréglage ?',
'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment',
'dialog.confirmAction' => 'Confirmer l\'action', 'dialog.confirmAction' => 'Confirmer l\'action',
@@ -2845,10 +2849,10 @@ extension on TranslationsFr {
'externalPlayer.playerPackage' => 'Nom du paquet', 'externalPlayer.playerPackage' => 'Nom du paquet',
'externalPlayer.playerUrlScheme' => 'Schéma URL', 'externalPlayer.playerUrlScheme' => 'Schéma URL',
'externalPlayer.off' => 'Désactivé', 'externalPlayer.off' => 'Désactivé',
'externalPlayer.launchFailed' => 'Impossible d\'ouvrir le lecteur externe',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} n\'est pas installé',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'externalPlayer.launchFailed' => 'Impossible d\'ouvrir le lecteur externe',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} n\'est pas installé',
'externalPlayer.playInExternalPlayer' => 'Lire dans un lecteur externe', 'externalPlayer.playInExternalPlayer' => 'Lire dans un lecteur externe',
'metadataEdit.editMetadata' => 'Modifier...', 'metadataEdit.editMetadata' => 'Modifier...',
'metadataEdit.screenTitle' => 'Modifier les métadonnées', 'metadataEdit.screenTitle' => 'Modifier les métadonnées',
+8 -4
View File
@@ -672,8 +672,10 @@ class _TranslationsMessagesIt extends TranslationsMessagesEn {
@override String metadataRefreshFailed({required Object error}) => 'Errore aggiornamento metadati: ${error}'; @override String metadataRefreshFailed({required Object error}) => 'Errore aggiornamento metadati: ${error}';
@override String get logoutConfirm => 'Sei sicuro di volerti disconnettere?'; @override String get logoutConfirm => 'Sei sicuro di volerti disconnettere?';
@override String get noSeasonsFound => 'Nessuna stagione trovata'; @override String get noSeasonsFound => 'Nessuna stagione trovata';
@override String get seasonsLoadFailed => 'Impossibile caricare le stagioni';
@override String get noEpisodesFound => 'Nessun episodio trovato nella prima stagione'; @override String get noEpisodesFound => 'Nessun episodio trovato nella prima stagione';
@override String get noEpisodesFoundGeneral => 'Nessun episodio trovato'; @override String get noEpisodesFoundGeneral => 'Nessun episodio trovato';
@override String get episodesLoadFailed => 'Impossibile caricare gli episodi';
@override String get noResultsFound => 'Nessun risultato'; @override String get noResultsFound => 'Nessun risultato';
@override String sleepTimerSet({required Object label}) => 'Imposta timer spegnimento per ${label}'; @override String sleepTimerSet({required Object label}) => 'Imposta timer spegnimento per ${label}';
@override String get noItemsAvailable => 'Nessun elemento disponibile'; @override String get noItemsAvailable => 'Nessun elemento disponibile';
@@ -2295,8 +2297,10 @@ extension on TranslationsIt {
'messages.metadataRefreshFailed' => ({required Object error}) => 'Errore aggiornamento metadati: ${error}', 'messages.metadataRefreshFailed' => ({required Object error}) => 'Errore aggiornamento metadati: ${error}',
'messages.logoutConfirm' => 'Sei sicuro di volerti disconnettere?', 'messages.logoutConfirm' => 'Sei sicuro di volerti disconnettere?',
'messages.noSeasonsFound' => 'Nessuna stagione trovata', 'messages.noSeasonsFound' => 'Nessuna stagione trovata',
'messages.seasonsLoadFailed' => 'Impossibile caricare le stagioni',
'messages.noEpisodesFound' => 'Nessun episodio trovato nella prima stagione', 'messages.noEpisodesFound' => 'Nessun episodio trovato nella prima stagione',
'messages.noEpisodesFoundGeneral' => 'Nessun episodio trovato', 'messages.noEpisodesFoundGeneral' => 'Nessun episodio trovato',
'messages.episodesLoadFailed' => 'Impossibile caricare gli episodi',
'messages.noResultsFound' => 'Nessun risultato', 'messages.noResultsFound' => 'Nessun risultato',
'messages.sleepTimerSet' => ({required Object label}) => 'Imposta timer spegnimento per ${label}', 'messages.sleepTimerSet' => ({required Object label}) => 'Imposta timer spegnimento per ${label}',
'messages.noItemsAvailable' => 'Nessun elemento disponibile', 'messages.noItemsAvailable' => 'Nessun elemento disponibile',
@@ -2331,10 +2335,10 @@ extension on TranslationsIt {
'mpvConfig.loadPreset' => 'Carica', 'mpvConfig.loadPreset' => 'Carica',
'mpvConfig.deletePreset' => 'Elimina', 'mpvConfig.deletePreset' => 'Elimina',
'mpvConfig.presetSaved' => 'Preset salvato', 'mpvConfig.presetSaved' => 'Preset salvato',
'mpvConfig.presetLoaded' => 'Preset caricato',
'mpvConfig.presetDeleted' => 'Preset eliminato',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'mpvConfig.presetLoaded' => 'Preset caricato',
'mpvConfig.presetDeleted' => 'Preset eliminato',
'mpvConfig.confirmDeletePreset' => 'Sei sicuro di voler eliminare questo preset?', 'mpvConfig.confirmDeletePreset' => 'Sei sicuro di voler eliminare questo preset?',
'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment',
'dialog.confirmAction' => 'Conferma azione', 'dialog.confirmAction' => 'Conferma azione',
@@ -2845,10 +2849,10 @@ extension on TranslationsIt {
'externalPlayer.playerPackage' => 'Nome pacchetto', 'externalPlayer.playerPackage' => 'Nome pacchetto',
'externalPlayer.playerUrlScheme' => 'Schema URL', 'externalPlayer.playerUrlScheme' => 'Schema URL',
'externalPlayer.off' => 'Disattivato', 'externalPlayer.off' => 'Disattivato',
'externalPlayer.launchFailed' => 'Impossibile aprire il lettore esterno',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} non è installato',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'externalPlayer.launchFailed' => 'Impossibile aprire il lettore esterno',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} non è installato',
'externalPlayer.playInExternalPlayer' => 'Riproduci in lettore esterno', 'externalPlayer.playInExternalPlayer' => 'Riproduci in lettore esterno',
'metadataEdit.editMetadata' => 'Modifica...', 'metadataEdit.editMetadata' => 'Modifica...',
'metadataEdit.screenTitle' => 'Modifica metadati', 'metadataEdit.screenTitle' => 'Modifica metadati',
+8 -4
View File
@@ -672,8 +672,10 @@ class _TranslationsMessagesJa extends TranslationsMessagesEn {
@override String metadataRefreshFailed({required Object error}) => 'メタデータの更新に失敗しました: ${error}'; @override String metadataRefreshFailed({required Object error}) => 'メタデータの更新に失敗しました: ${error}';
@override String get logoutConfirm => 'ログアウトしてもよろしいですか?'; @override String get logoutConfirm => 'ログアウトしてもよろしいですか?';
@override String get noSeasonsFound => 'シーズンが見つかりません'; @override String get noSeasonsFound => 'シーズンが見つかりません';
@override String get seasonsLoadFailed => 'シーズンを読み込めませんでした';
@override String get noEpisodesFound => '最初のシーズンにエピソードが見つかりません'; @override String get noEpisodesFound => '最初のシーズンにエピソードが見つかりません';
@override String get noEpisodesFoundGeneral => 'エピソードが見つかりません'; @override String get noEpisodesFoundGeneral => 'エピソードが見つかりません';
@override String get episodesLoadFailed => 'エピソードを読み込めませんでした';
@override String get noResultsFound => '結果が見つかりません'; @override String get noResultsFound => '結果が見つかりません';
@override String sleepTimerSet({required Object label}) => 'スリープタイマーを${label}に設定しました'; @override String sleepTimerSet({required Object label}) => 'スリープタイマーを${label}に設定しました';
@override String get noItemsAvailable => 'アイテムがありません'; @override String get noItemsAvailable => 'アイテムがありません';
@@ -2295,8 +2297,10 @@ extension on TranslationsJa {
'messages.metadataRefreshFailed' => ({required Object error}) => 'メタデータの更新に失敗しました: ${error}', 'messages.metadataRefreshFailed' => ({required Object error}) => 'メタデータの更新に失敗しました: ${error}',
'messages.logoutConfirm' => 'ログアウトしてもよろしいですか?', 'messages.logoutConfirm' => 'ログアウトしてもよろしいですか?',
'messages.noSeasonsFound' => 'シーズンが見つかりません', 'messages.noSeasonsFound' => 'シーズンが見つかりません',
'messages.seasonsLoadFailed' => 'シーズンを読み込めませんでした',
'messages.noEpisodesFound' => '最初のシーズンにエピソードが見つかりません', 'messages.noEpisodesFound' => '最初のシーズンにエピソードが見つかりません',
'messages.noEpisodesFoundGeneral' => 'エピソードが見つかりません', 'messages.noEpisodesFoundGeneral' => 'エピソードが見つかりません',
'messages.episodesLoadFailed' => 'エピソードを読み込めませんでした',
'messages.noResultsFound' => '結果が見つかりません', 'messages.noResultsFound' => '結果が見つかりません',
'messages.sleepTimerSet' => ({required Object label}) => 'スリープタイマーを${label}に設定しました', 'messages.sleepTimerSet' => ({required Object label}) => 'スリープタイマーを${label}に設定しました',
'messages.noItemsAvailable' => 'アイテムがありません', 'messages.noItemsAvailable' => 'アイテムがありません',
@@ -2331,10 +2335,10 @@ extension on TranslationsJa {
'mpvConfig.loadPreset' => '読み込み', 'mpvConfig.loadPreset' => '読み込み',
'mpvConfig.deletePreset' => '削除', 'mpvConfig.deletePreset' => '削除',
'mpvConfig.presetSaved' => 'プリセットを保存しました', 'mpvConfig.presetSaved' => 'プリセットを保存しました',
'mpvConfig.presetLoaded' => 'プリセットを読み込みました',
'mpvConfig.presetDeleted' => 'プリセットを削除しました',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'mpvConfig.presetLoaded' => 'プリセットを読み込みました',
'mpvConfig.presetDeleted' => 'プリセットを削除しました',
'mpvConfig.confirmDeletePreset' => 'このプリセットを削除してもよろしいですか?', 'mpvConfig.confirmDeletePreset' => 'このプリセットを削除してもよろしいですか?',
'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment',
'dialog.confirmAction' => '操作の確認', 'dialog.confirmAction' => '操作の確認',
@@ -2845,10 +2849,10 @@ extension on TranslationsJa {
'externalPlayer.playerPackage' => 'パッケージ名', 'externalPlayer.playerPackage' => 'パッケージ名',
'externalPlayer.playerUrlScheme' => 'URLスキーム', 'externalPlayer.playerUrlScheme' => 'URLスキーム',
'externalPlayer.off' => 'オフ', 'externalPlayer.off' => 'オフ',
'externalPlayer.launchFailed' => '外部プレーヤーの起動に失敗しました',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name}がインストールされていません',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'externalPlayer.launchFailed' => '外部プレーヤーの起動に失敗しました',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name}がインストールされていません',
'externalPlayer.playInExternalPlayer' => '外部プレーヤーで再生', 'externalPlayer.playInExternalPlayer' => '外部プレーヤーで再生',
'metadataEdit.editMetadata' => '編集...', 'metadataEdit.editMetadata' => '編集...',
'metadataEdit.screenTitle' => 'メタデータを編集', 'metadataEdit.screenTitle' => 'メタデータを編集',
+8 -4
View File
@@ -672,8 +672,10 @@ class _TranslationsMessagesKo extends TranslationsMessagesEn {
@override String metadataRefreshFailed({required Object error}) => '메타데이터 새로고침 실패: ${error}'; @override String metadataRefreshFailed({required Object error}) => '메타데이터 새로고침 실패: ${error}';
@override String get logoutConfirm => '로그아웃 하시겠습니까?'; @override String get logoutConfirm => '로그아웃 하시겠습니까?';
@override String get noSeasonsFound => '시즌을 찾을 수 없음'; @override String get noSeasonsFound => '시즌을 찾을 수 없음';
@override String get seasonsLoadFailed => '시즌을 불러오지 못했습니다';
@override String get noEpisodesFound => '시즌 1에서 에피소드를 찾을 수 없습니다'; @override String get noEpisodesFound => '시즌 1에서 에피소드를 찾을 수 없습니다';
@override String get noEpisodesFoundGeneral => '에피소드를 찾을 수 없습니다'; @override String get noEpisodesFoundGeneral => '에피소드를 찾을 수 없습니다';
@override String get episodesLoadFailed => '에피소드를 불러오지 못했습니다';
@override String get noResultsFound => '결과를 찾을 수 없습니다'; @override String get noResultsFound => '결과를 찾을 수 없습니다';
@override String sleepTimerSet({required Object label}) => '수면 타이머가 ${label}로 설정 되었습니다'; @override String sleepTimerSet({required Object label}) => '수면 타이머가 ${label}로 설정 되었습니다';
@override String get noItemsAvailable => '사용 가능한 항목이 없습니다'; @override String get noItemsAvailable => '사용 가능한 항목이 없습니다';
@@ -2295,8 +2297,10 @@ extension on TranslationsKo {
'messages.metadataRefreshFailed' => ({required Object error}) => '메타데이터 새로고침 실패: ${error}', 'messages.metadataRefreshFailed' => ({required Object error}) => '메타데이터 새로고침 실패: ${error}',
'messages.logoutConfirm' => '로그아웃 하시겠습니까?', 'messages.logoutConfirm' => '로그아웃 하시겠습니까?',
'messages.noSeasonsFound' => '시즌을 찾을 수 없음', 'messages.noSeasonsFound' => '시즌을 찾을 수 없음',
'messages.seasonsLoadFailed' => '시즌을 불러오지 못했습니다',
'messages.noEpisodesFound' => '시즌 1에서 에피소드를 찾을 수 없습니다', 'messages.noEpisodesFound' => '시즌 1에서 에피소드를 찾을 수 없습니다',
'messages.noEpisodesFoundGeneral' => '에피소드를 찾을 수 없습니다', 'messages.noEpisodesFoundGeneral' => '에피소드를 찾을 수 없습니다',
'messages.episodesLoadFailed' => '에피소드를 불러오지 못했습니다',
'messages.noResultsFound' => '결과를 찾을 수 없습니다', 'messages.noResultsFound' => '결과를 찾을 수 없습니다',
'messages.sleepTimerSet' => ({required Object label}) => '수면 타이머가 ${label}로 설정 되었습니다', 'messages.sleepTimerSet' => ({required Object label}) => '수면 타이머가 ${label}로 설정 되었습니다',
'messages.noItemsAvailable' => '사용 가능한 항목이 없습니다', 'messages.noItemsAvailable' => '사용 가능한 항목이 없습니다',
@@ -2331,10 +2335,10 @@ extension on TranslationsKo {
'mpvConfig.loadPreset' => '로드', 'mpvConfig.loadPreset' => '로드',
'mpvConfig.deletePreset' => '삭제', 'mpvConfig.deletePreset' => '삭제',
'mpvConfig.presetSaved' => '프리셋이 저장 되었습니다', 'mpvConfig.presetSaved' => '프리셋이 저장 되었습니다',
'mpvConfig.presetLoaded' => '프리셋이 로드 되었습니다',
'mpvConfig.presetDeleted' => '프리셋이 삭제 되었습니다',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'mpvConfig.presetLoaded' => '프리셋이 로드 되었습니다',
'mpvConfig.presetDeleted' => '프리셋이 삭제 되었습니다',
'mpvConfig.confirmDeletePreset' => '이 프리셋을 삭제 하시겠습니까?', 'mpvConfig.confirmDeletePreset' => '이 프리셋을 삭제 하시겠습니까?',
'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment',
'dialog.confirmAction' => '확인', 'dialog.confirmAction' => '확인',
@@ -2845,10 +2849,10 @@ extension on TranslationsKo {
'externalPlayer.playerPackage' => '패키지 이름', 'externalPlayer.playerPackage' => '패키지 이름',
'externalPlayer.playerUrlScheme' => 'URL 스킴', 'externalPlayer.playerUrlScheme' => 'URL 스킴',
'externalPlayer.off' => '꺼짐', 'externalPlayer.off' => '꺼짐',
'externalPlayer.launchFailed' => '외부 플레이어를 열 수 없습니다',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name}이(가) 설치되어 있지 않습니다',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'externalPlayer.launchFailed' => '외부 플레이어를 열 수 없습니다',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name}이(가) 설치되어 있지 않습니다',
'externalPlayer.playInExternalPlayer' => '외부 플레이어에서 재생', 'externalPlayer.playInExternalPlayer' => '외부 플레이어에서 재생',
'metadataEdit.editMetadata' => '편집...', 'metadataEdit.editMetadata' => '편집...',
'metadataEdit.screenTitle' => '메타데이터 편집', 'metadataEdit.screenTitle' => '메타데이터 편집',
+8 -4
View File
@@ -672,8 +672,10 @@ class _TranslationsMessagesNb extends TranslationsMessagesEn {
@override String metadataRefreshFailed({required Object error}) => 'Kunne ikke oppdatere metadata: ${error}'; @override String metadataRefreshFailed({required Object error}) => 'Kunne ikke oppdatere metadata: ${error}';
@override String get logoutConfirm => 'Er du sikker på at du vil logge ut?'; @override String get logoutConfirm => 'Er du sikker på at du vil logge ut?';
@override String get noSeasonsFound => 'Ingen sesonger funnet'; @override String get noSeasonsFound => 'Ingen sesonger funnet';
@override String get seasonsLoadFailed => 'Kunne ikke laste sesonger';
@override String get noEpisodesFound => 'Ingen episoder funnet i første sesong'; @override String get noEpisodesFound => 'Ingen episoder funnet i første sesong';
@override String get noEpisodesFoundGeneral => 'Ingen episoder funnet'; @override String get noEpisodesFoundGeneral => 'Ingen episoder funnet';
@override String get episodesLoadFailed => 'Kunne ikke laste episoder';
@override String get noResultsFound => 'Ingen resultater funnet'; @override String get noResultsFound => 'Ingen resultater funnet';
@override String sleepTimerSet({required Object label}) => 'Søvntimer satt til ${label}'; @override String sleepTimerSet({required Object label}) => 'Søvntimer satt til ${label}';
@override String get noItemsAvailable => 'Ingen elementer tilgjengelig'; @override String get noItemsAvailable => 'Ingen elementer tilgjengelig';
@@ -2295,8 +2297,10 @@ extension on TranslationsNb {
'messages.metadataRefreshFailed' => ({required Object error}) => 'Kunne ikke oppdatere metadata: ${error}', 'messages.metadataRefreshFailed' => ({required Object error}) => 'Kunne ikke oppdatere metadata: ${error}',
'messages.logoutConfirm' => 'Er du sikker på at du vil logge ut?', 'messages.logoutConfirm' => 'Er du sikker på at du vil logge ut?',
'messages.noSeasonsFound' => 'Ingen sesonger funnet', 'messages.noSeasonsFound' => 'Ingen sesonger funnet',
'messages.seasonsLoadFailed' => 'Kunne ikke laste sesonger',
'messages.noEpisodesFound' => 'Ingen episoder funnet i første sesong', 'messages.noEpisodesFound' => 'Ingen episoder funnet i første sesong',
'messages.noEpisodesFoundGeneral' => 'Ingen episoder funnet', 'messages.noEpisodesFoundGeneral' => 'Ingen episoder funnet',
'messages.episodesLoadFailed' => 'Kunne ikke laste episoder',
'messages.noResultsFound' => 'Ingen resultater funnet', 'messages.noResultsFound' => 'Ingen resultater funnet',
'messages.sleepTimerSet' => ({required Object label}) => 'Søvntimer satt til ${label}', 'messages.sleepTimerSet' => ({required Object label}) => 'Søvntimer satt til ${label}',
'messages.noItemsAvailable' => 'Ingen elementer tilgjengelig', 'messages.noItemsAvailable' => 'Ingen elementer tilgjengelig',
@@ -2331,10 +2335,10 @@ extension on TranslationsNb {
'mpvConfig.loadPreset' => 'Last inn', 'mpvConfig.loadPreset' => 'Last inn',
'mpvConfig.deletePreset' => 'Slett', 'mpvConfig.deletePreset' => 'Slett',
'mpvConfig.presetSaved' => 'Forhåndsinnstilling lagret', 'mpvConfig.presetSaved' => 'Forhåndsinnstilling lagret',
'mpvConfig.presetLoaded' => 'Forhåndsinnstilling lastet inn',
'mpvConfig.presetDeleted' => 'Forhåndsinnstilling slettet',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'mpvConfig.presetLoaded' => 'Forhåndsinnstilling lastet inn',
'mpvConfig.presetDeleted' => 'Forhåndsinnstilling slettet',
'mpvConfig.confirmDeletePreset' => 'Er du sikker på at du vil slette denne forhåndsinnstillingen?', 'mpvConfig.confirmDeletePreset' => 'Er du sikker på at du vil slette denne forhåndsinnstillingen?',
'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# kommentar', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# kommentar',
'dialog.confirmAction' => 'Bekreft handling', 'dialog.confirmAction' => 'Bekreft handling',
@@ -2845,10 +2849,10 @@ extension on TranslationsNb {
'externalPlayer.playerPackage' => 'Pakkenavn', 'externalPlayer.playerPackage' => 'Pakkenavn',
'externalPlayer.playerUrlScheme' => 'URL-skjema', 'externalPlayer.playerUrlScheme' => 'URL-skjema',
'externalPlayer.off' => 'Av', 'externalPlayer.off' => 'Av',
'externalPlayer.launchFailed' => 'Kunne ikke åpne ekstern spiller',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} er ikke installert',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'externalPlayer.launchFailed' => 'Kunne ikke åpne ekstern spiller',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} er ikke installert',
'externalPlayer.playInExternalPlayer' => 'Spill av i ekstern spiller', 'externalPlayer.playInExternalPlayer' => 'Spill av i ekstern spiller',
'metadataEdit.editMetadata' => 'Rediger...', 'metadataEdit.editMetadata' => 'Rediger...',
'metadataEdit.screenTitle' => 'Rediger metadata', 'metadataEdit.screenTitle' => 'Rediger metadata',
+8 -4
View File
@@ -672,8 +672,10 @@ class _TranslationsMessagesNl extends TranslationsMessagesEn {
@override String metadataRefreshFailed({required Object error}) => 'Kon metadata niet vernieuwen: ${error}'; @override String metadataRefreshFailed({required Object error}) => 'Kon metadata niet vernieuwen: ${error}';
@override String get logoutConfirm => 'Weet je zeker dat je wilt uitloggen?'; @override String get logoutConfirm => 'Weet je zeker dat je wilt uitloggen?';
@override String get noSeasonsFound => 'Geen seizoenen gevonden'; @override String get noSeasonsFound => 'Geen seizoenen gevonden';
@override String get seasonsLoadFailed => 'Kan seizoenen niet laden';
@override String get noEpisodesFound => 'Geen afleveringen gevonden in eerste seizoen'; @override String get noEpisodesFound => 'Geen afleveringen gevonden in eerste seizoen';
@override String get noEpisodesFoundGeneral => 'Geen afleveringen gevonden'; @override String get noEpisodesFoundGeneral => 'Geen afleveringen gevonden';
@override String get episodesLoadFailed => 'Kan afleveringen niet laden';
@override String get noResultsFound => 'Geen resultaten gevonden'; @override String get noResultsFound => 'Geen resultaten gevonden';
@override String sleepTimerSet({required Object label}) => 'Slaap timer ingesteld voor ${label}'; @override String sleepTimerSet({required Object label}) => 'Slaap timer ingesteld voor ${label}';
@override String get noItemsAvailable => 'Geen items beschikbaar'; @override String get noItemsAvailable => 'Geen items beschikbaar';
@@ -2295,8 +2297,10 @@ extension on TranslationsNl {
'messages.metadataRefreshFailed' => ({required Object error}) => 'Kon metadata niet vernieuwen: ${error}', 'messages.metadataRefreshFailed' => ({required Object error}) => 'Kon metadata niet vernieuwen: ${error}',
'messages.logoutConfirm' => 'Weet je zeker dat je wilt uitloggen?', 'messages.logoutConfirm' => 'Weet je zeker dat je wilt uitloggen?',
'messages.noSeasonsFound' => 'Geen seizoenen gevonden', 'messages.noSeasonsFound' => 'Geen seizoenen gevonden',
'messages.seasonsLoadFailed' => 'Kan seizoenen niet laden',
'messages.noEpisodesFound' => 'Geen afleveringen gevonden in eerste seizoen', 'messages.noEpisodesFound' => 'Geen afleveringen gevonden in eerste seizoen',
'messages.noEpisodesFoundGeneral' => 'Geen afleveringen gevonden', 'messages.noEpisodesFoundGeneral' => 'Geen afleveringen gevonden',
'messages.episodesLoadFailed' => 'Kan afleveringen niet laden',
'messages.noResultsFound' => 'Geen resultaten gevonden', 'messages.noResultsFound' => 'Geen resultaten gevonden',
'messages.sleepTimerSet' => ({required Object label}) => 'Slaap timer ingesteld voor ${label}', 'messages.sleepTimerSet' => ({required Object label}) => 'Slaap timer ingesteld voor ${label}',
'messages.noItemsAvailable' => 'Geen items beschikbaar', 'messages.noItemsAvailable' => 'Geen items beschikbaar',
@@ -2331,10 +2335,10 @@ extension on TranslationsNl {
'mpvConfig.loadPreset' => 'Laden', 'mpvConfig.loadPreset' => 'Laden',
'mpvConfig.deletePreset' => 'Verwijderen', 'mpvConfig.deletePreset' => 'Verwijderen',
'mpvConfig.presetSaved' => 'Voorinstelling opgeslagen', 'mpvConfig.presetSaved' => 'Voorinstelling opgeslagen',
'mpvConfig.presetLoaded' => 'Voorinstelling geladen',
'mpvConfig.presetDeleted' => 'Voorinstelling verwijderd',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'mpvConfig.presetLoaded' => 'Voorinstelling geladen',
'mpvConfig.presetDeleted' => 'Voorinstelling verwijderd',
'mpvConfig.confirmDeletePreset' => 'Weet je zeker dat je deze voorinstelling wilt verwijderen?', 'mpvConfig.confirmDeletePreset' => 'Weet je zeker dat je deze voorinstelling wilt verwijderen?',
'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment',
'dialog.confirmAction' => 'Bevestig actie', 'dialog.confirmAction' => 'Bevestig actie',
@@ -2845,10 +2849,10 @@ extension on TranslationsNl {
'externalPlayer.playerPackage' => 'Pakketnaam', 'externalPlayer.playerPackage' => 'Pakketnaam',
'externalPlayer.playerUrlScheme' => 'URL-schema', 'externalPlayer.playerUrlScheme' => 'URL-schema',
'externalPlayer.off' => 'Uit', 'externalPlayer.off' => 'Uit',
'externalPlayer.launchFailed' => 'Kan externe speler niet openen',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} is niet geïnstalleerd',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'externalPlayer.launchFailed' => 'Kan externe speler niet openen',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} is niet geïnstalleerd',
'externalPlayer.playInExternalPlayer' => 'Afspelen in externe speler', 'externalPlayer.playInExternalPlayer' => 'Afspelen in externe speler',
'metadataEdit.editMetadata' => 'Bewerken...', 'metadataEdit.editMetadata' => 'Bewerken...',
'metadataEdit.screenTitle' => 'Metadata bewerken', 'metadataEdit.screenTitle' => 'Metadata bewerken',
+8 -4
View File
@@ -672,8 +672,10 @@ class _TranslationsMessagesPl extends TranslationsMessagesEn {
@override String metadataRefreshFailed({required Object error}) => 'Nie udało się odświeżyć metadanych: ${error}'; @override String metadataRefreshFailed({required Object error}) => 'Nie udało się odświeżyć metadanych: ${error}';
@override String get logoutConfirm => 'Czy na pewno chcesz się wylogować?'; @override String get logoutConfirm => 'Czy na pewno chcesz się wylogować?';
@override String get noSeasonsFound => 'Nie znaleziono sezonów'; @override String get noSeasonsFound => 'Nie znaleziono sezonów';
@override String get seasonsLoadFailed => 'Nie udało się załadować sezonów';
@override String get noEpisodesFound => 'Nie znaleziono odcinków w pierwszym sezonie'; @override String get noEpisodesFound => 'Nie znaleziono odcinków w pierwszym sezonie';
@override String get noEpisodesFoundGeneral => 'Nie znaleziono odcinków'; @override String get noEpisodesFoundGeneral => 'Nie znaleziono odcinków';
@override String get episodesLoadFailed => 'Nie udało się załadować odcinków';
@override String get noResultsFound => 'Nie znaleziono wyników'; @override String get noResultsFound => 'Nie znaleziono wyników';
@override String sleepTimerSet({required Object label}) => 'Wyłącznik czasowy ustawiony na ${label}'; @override String sleepTimerSet({required Object label}) => 'Wyłącznik czasowy ustawiony na ${label}';
@override String get noItemsAvailable => 'Brak dostępnych elementów'; @override String get noItemsAvailable => 'Brak dostępnych elementów';
@@ -2295,8 +2297,10 @@ extension on TranslationsPl {
'messages.metadataRefreshFailed' => ({required Object error}) => 'Nie udało się odświeżyć metadanych: ${error}', 'messages.metadataRefreshFailed' => ({required Object error}) => 'Nie udało się odświeżyć metadanych: ${error}',
'messages.logoutConfirm' => 'Czy na pewno chcesz się wylogować?', 'messages.logoutConfirm' => 'Czy na pewno chcesz się wylogować?',
'messages.noSeasonsFound' => 'Nie znaleziono sezonów', 'messages.noSeasonsFound' => 'Nie znaleziono sezonów',
'messages.seasonsLoadFailed' => 'Nie udało się załadować sezonów',
'messages.noEpisodesFound' => 'Nie znaleziono odcinków w pierwszym sezonie', 'messages.noEpisodesFound' => 'Nie znaleziono odcinków w pierwszym sezonie',
'messages.noEpisodesFoundGeneral' => 'Nie znaleziono odcinków', 'messages.noEpisodesFoundGeneral' => 'Nie znaleziono odcinków',
'messages.episodesLoadFailed' => 'Nie udało się załadować odcinków',
'messages.noResultsFound' => 'Nie znaleziono wyników', 'messages.noResultsFound' => 'Nie znaleziono wyników',
'messages.sleepTimerSet' => ({required Object label}) => 'Wyłącznik czasowy ustawiony na ${label}', 'messages.sleepTimerSet' => ({required Object label}) => 'Wyłącznik czasowy ustawiony na ${label}',
'messages.noItemsAvailable' => 'Brak dostępnych elementów', 'messages.noItemsAvailable' => 'Brak dostępnych elementów',
@@ -2331,10 +2335,10 @@ extension on TranslationsPl {
'mpvConfig.loadPreset' => 'Załaduj', 'mpvConfig.loadPreset' => 'Załaduj',
'mpvConfig.deletePreset' => 'Usuń', 'mpvConfig.deletePreset' => 'Usuń',
'mpvConfig.presetSaved' => 'Preset zapisany', 'mpvConfig.presetSaved' => 'Preset zapisany',
'mpvConfig.presetLoaded' => 'Preset załadowany',
'mpvConfig.presetDeleted' => 'Preset usunięty',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'mpvConfig.presetLoaded' => 'Preset załadowany',
'mpvConfig.presetDeleted' => 'Preset usunięty',
'mpvConfig.confirmDeletePreset' => 'Czy na pewno chcesz usunąć ten preset?', 'mpvConfig.confirmDeletePreset' => 'Czy na pewno chcesz usunąć ten preset?',
'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment',
'dialog.confirmAction' => 'Potwierdź działanie', 'dialog.confirmAction' => 'Potwierdź działanie',
@@ -2845,10 +2849,10 @@ extension on TranslationsPl {
'externalPlayer.playerPackage' => 'Nazwa pakietu', 'externalPlayer.playerPackage' => 'Nazwa pakietu',
'externalPlayer.playerUrlScheme' => 'Schemat URL', 'externalPlayer.playerUrlScheme' => 'Schemat URL',
'externalPlayer.off' => 'Wyłączony', 'externalPlayer.off' => 'Wyłączony',
'externalPlayer.launchFailed' => 'Nie udało się otworzyć zewnętrznego odtwarzacza',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} nie jest zainstalowany',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'externalPlayer.launchFailed' => 'Nie udało się otworzyć zewnętrznego odtwarzacza',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} nie jest zainstalowany',
'externalPlayer.playInExternalPlayer' => 'Odtwórz w zewnętrznym odtwarzaczu', 'externalPlayer.playInExternalPlayer' => 'Odtwórz w zewnętrznym odtwarzaczu',
'metadataEdit.editMetadata' => 'Edytuj...', 'metadataEdit.editMetadata' => 'Edytuj...',
'metadataEdit.screenTitle' => 'Edytuj metadane', 'metadataEdit.screenTitle' => 'Edytuj metadane',
+8 -4
View File
@@ -672,8 +672,10 @@ class _TranslationsMessagesPt extends TranslationsMessagesEn {
@override String metadataRefreshFailed({required Object error}) => 'Falha ao atualizar metadados: ${error}'; @override String metadataRefreshFailed({required Object error}) => 'Falha ao atualizar metadados: ${error}';
@override String get logoutConfirm => 'Tem certeza que deseja sair?'; @override String get logoutConfirm => 'Tem certeza que deseja sair?';
@override String get noSeasonsFound => 'Nenhuma temporada encontrada'; @override String get noSeasonsFound => 'Nenhuma temporada encontrada';
@override String get seasonsLoadFailed => 'Não foi possível carregar as temporadas';
@override String get noEpisodesFound => 'Nenhum episódio encontrado na primeira temporada'; @override String get noEpisodesFound => 'Nenhum episódio encontrado na primeira temporada';
@override String get noEpisodesFoundGeneral => 'Nenhum episódio encontrado'; @override String get noEpisodesFoundGeneral => 'Nenhum episódio encontrado';
@override String get episodesLoadFailed => 'Não foi possível carregar os episódios';
@override String get noResultsFound => 'Nenhum resultado encontrado'; @override String get noResultsFound => 'Nenhum resultado encontrado';
@override String sleepTimerSet({required Object label}) => 'Timer de sono definido para ${label}'; @override String sleepTimerSet({required Object label}) => 'Timer de sono definido para ${label}';
@override String get noItemsAvailable => 'Nenhum item disponível'; @override String get noItemsAvailable => 'Nenhum item disponível';
@@ -2295,8 +2297,10 @@ extension on TranslationsPt {
'messages.metadataRefreshFailed' => ({required Object error}) => 'Falha ao atualizar metadados: ${error}', 'messages.metadataRefreshFailed' => ({required Object error}) => 'Falha ao atualizar metadados: ${error}',
'messages.logoutConfirm' => 'Tem certeza que deseja sair?', 'messages.logoutConfirm' => 'Tem certeza que deseja sair?',
'messages.noSeasonsFound' => 'Nenhuma temporada encontrada', 'messages.noSeasonsFound' => 'Nenhuma temporada encontrada',
'messages.seasonsLoadFailed' => 'Não foi possível carregar as temporadas',
'messages.noEpisodesFound' => 'Nenhum episódio encontrado na primeira temporada', 'messages.noEpisodesFound' => 'Nenhum episódio encontrado na primeira temporada',
'messages.noEpisodesFoundGeneral' => 'Nenhum episódio encontrado', 'messages.noEpisodesFoundGeneral' => 'Nenhum episódio encontrado',
'messages.episodesLoadFailed' => 'Não foi possível carregar os episódios',
'messages.noResultsFound' => 'Nenhum resultado encontrado', 'messages.noResultsFound' => 'Nenhum resultado encontrado',
'messages.sleepTimerSet' => ({required Object label}) => 'Timer de sono definido para ${label}', 'messages.sleepTimerSet' => ({required Object label}) => 'Timer de sono definido para ${label}',
'messages.noItemsAvailable' => 'Nenhum item disponível', 'messages.noItemsAvailable' => 'Nenhum item disponível',
@@ -2331,10 +2335,10 @@ extension on TranslationsPt {
'mpvConfig.loadPreset' => 'Carregar', 'mpvConfig.loadPreset' => 'Carregar',
'mpvConfig.deletePreset' => 'Excluir', 'mpvConfig.deletePreset' => 'Excluir',
'mpvConfig.presetSaved' => 'Predefinição salva', 'mpvConfig.presetSaved' => 'Predefinição salva',
'mpvConfig.presetLoaded' => 'Predefinição carregada',
'mpvConfig.presetDeleted' => 'Predefinição excluída',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'mpvConfig.presetLoaded' => 'Predefinição carregada',
'mpvConfig.presetDeleted' => 'Predefinição excluída',
'mpvConfig.confirmDeletePreset' => 'Tem certeza que deseja excluir esta predefinição?', 'mpvConfig.confirmDeletePreset' => 'Tem certeza que deseja excluir esta predefinição?',
'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment',
'dialog.confirmAction' => 'Confirmar Ação', 'dialog.confirmAction' => 'Confirmar Ação',
@@ -2845,10 +2849,10 @@ extension on TranslationsPt {
'externalPlayer.playerPackage' => 'Nome do Pacote', 'externalPlayer.playerPackage' => 'Nome do Pacote',
'externalPlayer.playerUrlScheme' => 'Esquema de URL', 'externalPlayer.playerUrlScheme' => 'Esquema de URL',
'externalPlayer.off' => 'Desativado', 'externalPlayer.off' => 'Desativado',
'externalPlayer.launchFailed' => 'Falha ao abrir player externo',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} não está instalado',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'externalPlayer.launchFailed' => 'Falha ao abrir player externo',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} não está instalado',
'externalPlayer.playInExternalPlayer' => 'Reproduzir no Player Externo', 'externalPlayer.playInExternalPlayer' => 'Reproduzir no Player Externo',
'metadataEdit.editMetadata' => 'Editar...', 'metadataEdit.editMetadata' => 'Editar...',
'metadataEdit.screenTitle' => 'Editar Metadados', 'metadataEdit.screenTitle' => 'Editar Metadados',
+8 -4
View File
@@ -672,8 +672,10 @@ class _TranslationsMessagesRu extends TranslationsMessagesEn {
@override String metadataRefreshFailed({required Object error}) => 'Не удалось обновить метаданные: ${error}'; @override String metadataRefreshFailed({required Object error}) => 'Не удалось обновить метаданные: ${error}';
@override String get logoutConfirm => 'Вы уверены, что хотите выйти?'; @override String get logoutConfirm => 'Вы уверены, что хотите выйти?';
@override String get noSeasonsFound => 'Сезоны не найдены'; @override String get noSeasonsFound => 'Сезоны не найдены';
@override String get seasonsLoadFailed => 'Не удалось загрузить сезоны';
@override String get noEpisodesFound => 'Эпизоды в первом сезоне не найдены'; @override String get noEpisodesFound => 'Эпизоды в первом сезоне не найдены';
@override String get noEpisodesFoundGeneral => 'Эпизоды не найдены'; @override String get noEpisodesFoundGeneral => 'Эпизоды не найдены';
@override String get episodesLoadFailed => 'Не удалось загрузить эпизоды';
@override String get noResultsFound => 'Результаты не найдены'; @override String get noResultsFound => 'Результаты не найдены';
@override String sleepTimerSet({required Object label}) => 'Таймер сна установлен на ${label}'; @override String sleepTimerSet({required Object label}) => 'Таймер сна установлен на ${label}';
@override String get noItemsAvailable => 'Нет доступных элементов'; @override String get noItemsAvailable => 'Нет доступных элементов';
@@ -2295,8 +2297,10 @@ extension on TranslationsRu {
'messages.metadataRefreshFailed' => ({required Object error}) => 'Не удалось обновить метаданные: ${error}', 'messages.metadataRefreshFailed' => ({required Object error}) => 'Не удалось обновить метаданные: ${error}',
'messages.logoutConfirm' => 'Вы уверены, что хотите выйти?', 'messages.logoutConfirm' => 'Вы уверены, что хотите выйти?',
'messages.noSeasonsFound' => 'Сезоны не найдены', 'messages.noSeasonsFound' => 'Сезоны не найдены',
'messages.seasonsLoadFailed' => 'Не удалось загрузить сезоны',
'messages.noEpisodesFound' => 'Эпизоды в первом сезоне не найдены', 'messages.noEpisodesFound' => 'Эпизоды в первом сезоне не найдены',
'messages.noEpisodesFoundGeneral' => 'Эпизоды не найдены', 'messages.noEpisodesFoundGeneral' => 'Эпизоды не найдены',
'messages.episodesLoadFailed' => 'Не удалось загрузить эпизоды',
'messages.noResultsFound' => 'Результаты не найдены', 'messages.noResultsFound' => 'Результаты не найдены',
'messages.sleepTimerSet' => ({required Object label}) => 'Таймер сна установлен на ${label}', 'messages.sleepTimerSet' => ({required Object label}) => 'Таймер сна установлен на ${label}',
'messages.noItemsAvailable' => 'Нет доступных элементов', 'messages.noItemsAvailable' => 'Нет доступных элементов',
@@ -2331,10 +2335,10 @@ extension on TranslationsRu {
'mpvConfig.loadPreset' => 'Загрузить', 'mpvConfig.loadPreset' => 'Загрузить',
'mpvConfig.deletePreset' => 'Удалить', 'mpvConfig.deletePreset' => 'Удалить',
'mpvConfig.presetSaved' => 'Пресет сохранён', 'mpvConfig.presetSaved' => 'Пресет сохранён',
'mpvConfig.presetLoaded' => 'Пресет загружен',
'mpvConfig.presetDeleted' => 'Пресет удалён',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'mpvConfig.presetLoaded' => 'Пресет загружен',
'mpvConfig.presetDeleted' => 'Пресет удалён',
'mpvConfig.confirmDeletePreset' => 'Вы уверены, что хотите удалить этот пресет?', 'mpvConfig.confirmDeletePreset' => 'Вы уверены, что хотите удалить этот пресет?',
'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment',
'dialog.confirmAction' => 'Подтвердить действие', 'dialog.confirmAction' => 'Подтвердить действие',
@@ -2845,10 +2849,10 @@ extension on TranslationsRu {
'externalPlayer.playerPackage' => 'Имя пакета', 'externalPlayer.playerPackage' => 'Имя пакета',
'externalPlayer.playerUrlScheme' => 'URL-схема', 'externalPlayer.playerUrlScheme' => 'URL-схема',
'externalPlayer.off' => 'Выкл.', 'externalPlayer.off' => 'Выкл.',
'externalPlayer.launchFailed' => 'Не удалось открыть внешний плеер',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} не установлен',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'externalPlayer.launchFailed' => 'Не удалось открыть внешний плеер',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} не установлен',
'externalPlayer.playInExternalPlayer' => 'Воспроизвести во внешнем плеере', 'externalPlayer.playInExternalPlayer' => 'Воспроизвести во внешнем плеере',
'metadataEdit.editMetadata' => 'Редактировать...', 'metadataEdit.editMetadata' => 'Редактировать...',
'metadataEdit.screenTitle' => 'Редактировать метаданные', 'metadataEdit.screenTitle' => 'Редактировать метаданные',
+8 -4
View File
@@ -672,8 +672,10 @@ class _TranslationsMessagesSv extends TranslationsMessagesEn {
@override String metadataRefreshFailed({required Object error}) => 'Misslyckades att uppdatera metadata: ${error}'; @override String metadataRefreshFailed({required Object error}) => 'Misslyckades att uppdatera metadata: ${error}';
@override String get logoutConfirm => 'Är du säker på att du vill logga ut?'; @override String get logoutConfirm => 'Är du säker på att du vill logga ut?';
@override String get noSeasonsFound => 'Inga säsonger hittades'; @override String get noSeasonsFound => 'Inga säsonger hittades';
@override String get seasonsLoadFailed => 'Det gick inte att läsa in säsonger';
@override String get noEpisodesFound => 'Inga avsnitt hittades i första säsongen'; @override String get noEpisodesFound => 'Inga avsnitt hittades i första säsongen';
@override String get noEpisodesFoundGeneral => 'Inga avsnitt hittades'; @override String get noEpisodesFoundGeneral => 'Inga avsnitt hittades';
@override String get episodesLoadFailed => 'Det gick inte att läsa in avsnitt';
@override String get noResultsFound => 'Inga resultat hittades'; @override String get noResultsFound => 'Inga resultat hittades';
@override String sleepTimerSet({required Object label}) => 'Sovtimer inställd för ${label}'; @override String sleepTimerSet({required Object label}) => 'Sovtimer inställd för ${label}';
@override String get noItemsAvailable => 'Inga objekt tillgängliga'; @override String get noItemsAvailable => 'Inga objekt tillgängliga';
@@ -2295,8 +2297,10 @@ extension on TranslationsSv {
'messages.metadataRefreshFailed' => ({required Object error}) => 'Misslyckades att uppdatera metadata: ${error}', 'messages.metadataRefreshFailed' => ({required Object error}) => 'Misslyckades att uppdatera metadata: ${error}',
'messages.logoutConfirm' => 'Är du säker på att du vill logga ut?', 'messages.logoutConfirm' => 'Är du säker på att du vill logga ut?',
'messages.noSeasonsFound' => 'Inga säsonger hittades', 'messages.noSeasonsFound' => 'Inga säsonger hittades',
'messages.seasonsLoadFailed' => 'Det gick inte att läsa in säsonger',
'messages.noEpisodesFound' => 'Inga avsnitt hittades i första säsongen', 'messages.noEpisodesFound' => 'Inga avsnitt hittades i första säsongen',
'messages.noEpisodesFoundGeneral' => 'Inga avsnitt hittades', 'messages.noEpisodesFoundGeneral' => 'Inga avsnitt hittades',
'messages.episodesLoadFailed' => 'Det gick inte att läsa in avsnitt',
'messages.noResultsFound' => 'Inga resultat hittades', 'messages.noResultsFound' => 'Inga resultat hittades',
'messages.sleepTimerSet' => ({required Object label}) => 'Sovtimer inställd för ${label}', 'messages.sleepTimerSet' => ({required Object label}) => 'Sovtimer inställd för ${label}',
'messages.noItemsAvailable' => 'Inga objekt tillgängliga', 'messages.noItemsAvailable' => 'Inga objekt tillgängliga',
@@ -2331,10 +2335,10 @@ extension on TranslationsSv {
'mpvConfig.loadPreset' => 'Ladda', 'mpvConfig.loadPreset' => 'Ladda',
'mpvConfig.deletePreset' => 'Ta bort', 'mpvConfig.deletePreset' => 'Ta bort',
'mpvConfig.presetSaved' => 'Förval sparat', 'mpvConfig.presetSaved' => 'Förval sparat',
'mpvConfig.presetLoaded' => 'Förval laddat',
'mpvConfig.presetDeleted' => 'Förval borttaget',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'mpvConfig.presetLoaded' => 'Förval laddat',
'mpvConfig.presetDeleted' => 'Förval borttaget',
'mpvConfig.confirmDeletePreset' => 'Är du säker på att du vill ta bort detta förval?', 'mpvConfig.confirmDeletePreset' => 'Är du säker på att du vill ta bort detta förval?',
'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment',
'dialog.confirmAction' => 'Bekräfta åtgärd', 'dialog.confirmAction' => 'Bekräfta åtgärd',
@@ -2845,10 +2849,10 @@ extension on TranslationsSv {
'externalPlayer.playerPackage' => 'Paketnamn', 'externalPlayer.playerPackage' => 'Paketnamn',
'externalPlayer.playerUrlScheme' => 'URL-schema', 'externalPlayer.playerUrlScheme' => 'URL-schema',
'externalPlayer.off' => 'Av', 'externalPlayer.off' => 'Av',
'externalPlayer.launchFailed' => 'Kunde inte öppna extern spelare',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} är inte installerad',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'externalPlayer.launchFailed' => 'Kunde inte öppna extern spelare',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} är inte installerad',
'externalPlayer.playInExternalPlayer' => 'Spela i extern spelare', 'externalPlayer.playInExternalPlayer' => 'Spela i extern spelare',
'metadataEdit.editMetadata' => 'Redigera...', 'metadataEdit.editMetadata' => 'Redigera...',
'metadataEdit.screenTitle' => 'Redigera metadata', 'metadataEdit.screenTitle' => 'Redigera metadata',
+8 -4
View File
@@ -672,8 +672,10 @@ class _TranslationsMessagesZh extends TranslationsMessagesEn {
@override String metadataRefreshFailed({required Object error}) => '无法刷新元数据: ${error}'; @override String metadataRefreshFailed({required Object error}) => '无法刷新元数据: ${error}';
@override String get logoutConfirm => '你确定要登出吗?'; @override String get logoutConfirm => '你确定要登出吗?';
@override String get noSeasonsFound => '未找到季'; @override String get noSeasonsFound => '未找到季';
@override String get seasonsLoadFailed => '无法加载季';
@override String get noEpisodesFound => '在第一季中未找到剧集'; @override String get noEpisodesFound => '在第一季中未找到剧集';
@override String get noEpisodesFoundGeneral => '未找到剧集'; @override String get noEpisodesFoundGeneral => '未找到剧集';
@override String get episodesLoadFailed => '无法加载剧集';
@override String get noResultsFound => '未找到结果'; @override String get noResultsFound => '未找到结果';
@override String sleepTimerSet({required Object label}) => '睡眠定时器已设置为 ${label}'; @override String sleepTimerSet({required Object label}) => '睡眠定时器已设置为 ${label}';
@override String get noItemsAvailable => '没有可用的项目'; @override String get noItemsAvailable => '没有可用的项目';
@@ -2295,8 +2297,10 @@ extension on TranslationsZh {
'messages.metadataRefreshFailed' => ({required Object error}) => '无法刷新元数据: ${error}', 'messages.metadataRefreshFailed' => ({required Object error}) => '无法刷新元数据: ${error}',
'messages.logoutConfirm' => '你确定要登出吗?', 'messages.logoutConfirm' => '你确定要登出吗?',
'messages.noSeasonsFound' => '未找到季', 'messages.noSeasonsFound' => '未找到季',
'messages.seasonsLoadFailed' => '无法加载季',
'messages.noEpisodesFound' => '在第一季中未找到剧集', 'messages.noEpisodesFound' => '在第一季中未找到剧集',
'messages.noEpisodesFoundGeneral' => '未找到剧集', 'messages.noEpisodesFoundGeneral' => '未找到剧集',
'messages.episodesLoadFailed' => '无法加载剧集',
'messages.noResultsFound' => '未找到结果', 'messages.noResultsFound' => '未找到结果',
'messages.sleepTimerSet' => ({required Object label}) => '睡眠定时器已设置为 ${label}', 'messages.sleepTimerSet' => ({required Object label}) => '睡眠定时器已设置为 ${label}',
'messages.noItemsAvailable' => '没有可用的项目', 'messages.noItemsAvailable' => '没有可用的项目',
@@ -2331,10 +2335,10 @@ extension on TranslationsZh {
'mpvConfig.loadPreset' => '加载', 'mpvConfig.loadPreset' => '加载',
'mpvConfig.deletePreset' => '删除', 'mpvConfig.deletePreset' => '删除',
'mpvConfig.presetSaved' => '预设已保存', 'mpvConfig.presetSaved' => '预设已保存',
'mpvConfig.presetLoaded' => '预设已加载',
'mpvConfig.presetDeleted' => '预设已删除',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'mpvConfig.presetLoaded' => '预设已加载',
'mpvConfig.presetDeleted' => '预设已删除',
'mpvConfig.confirmDeletePreset' => '确定要删除此预设吗?', 'mpvConfig.confirmDeletePreset' => '确定要删除此预设吗?',
'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment', 'mpvConfig.configPlaceholder' => 'gpu-api=vulkan\nhwdec=auto\n# comment',
'dialog.confirmAction' => '确认操作', 'dialog.confirmAction' => '确认操作',
@@ -2845,10 +2849,10 @@ extension on TranslationsZh {
'externalPlayer.playerPackage' => '包名', 'externalPlayer.playerPackage' => '包名',
'externalPlayer.playerUrlScheme' => 'URL 方案', 'externalPlayer.playerUrlScheme' => 'URL 方案',
'externalPlayer.off' => '关闭', 'externalPlayer.off' => '关闭',
'externalPlayer.launchFailed' => '无法打开外部播放器',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} 未安装',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'externalPlayer.launchFailed' => '无法打开外部播放器',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} 未安装',
'externalPlayer.playInExternalPlayer' => '在外部播放器中播放', 'externalPlayer.playInExternalPlayer' => '在外部播放器中播放',
'metadataEdit.editMetadata' => '编辑...', 'metadataEdit.editMetadata' => '编辑...',
'metadataEdit.screenTitle' => '编辑元数据', 'metadataEdit.screenTitle' => '编辑元数据',
+2
View File
@@ -508,8 +508,10 @@
"metadataRefreshFailed": "Misslyckades att uppdatera metadata: ${error}", "metadataRefreshFailed": "Misslyckades att uppdatera metadata: ${error}",
"logoutConfirm": "Är du säker på att du vill logga ut?", "logoutConfirm": "Är du säker på att du vill logga ut?",
"noSeasonsFound": "Inga säsonger hittades", "noSeasonsFound": "Inga säsonger hittades",
"seasonsLoadFailed": "Det gick inte att läsa in säsonger",
"noEpisodesFound": "Inga avsnitt hittades i första säsongen", "noEpisodesFound": "Inga avsnitt hittades i första säsongen",
"noEpisodesFoundGeneral": "Inga avsnitt hittades", "noEpisodesFoundGeneral": "Inga avsnitt hittades",
"episodesLoadFailed": "Det gick inte att läsa in avsnitt",
"noResultsFound": "Inga resultat hittades", "noResultsFound": "Inga resultat hittades",
"sleepTimerSet": "Sovtimer inställd för ${label}", "sleepTimerSet": "Sovtimer inställd för ${label}",
"noItemsAvailable": "Inga objekt tillgängliga", "noItemsAvailable": "Inga objekt tillgängliga",
+2
View File
@@ -508,8 +508,10 @@
"metadataRefreshFailed": "无法刷新元数据: ${error}", "metadataRefreshFailed": "无法刷新元数据: ${error}",
"logoutConfirm": "你确定要登出吗?", "logoutConfirm": "你确定要登出吗?",
"noSeasonsFound": "未找到季", "noSeasonsFound": "未找到季",
"seasonsLoadFailed": "无法加载季",
"noEpisodesFound": "在第一季中未找到剧集", "noEpisodesFound": "在第一季中未找到剧集",
"noEpisodesFoundGeneral": "未找到剧集", "noEpisodesFoundGeneral": "未找到剧集",
"episodesLoadFailed": "无法加载剧集",
"noResultsFound": "未找到结果", "noResultsFound": "未找到结果",
"sleepTimerSet": "睡眠定时器已设置为 ${label}", "sleepTimerSet": "睡眠定时器已设置为 ${label}",
"noItemsAvailable": "没有可用的项目", "noItemsAvailable": "没有可用的项目",
+172
View File
@@ -0,0 +1,172 @@
import 'library_query.dart';
import 'media_item.dart';
import 'media_kind.dart';
import 'media_server_client.dart';
/// Collect every episode of a show into [out] using the backend's one-shot
/// recursive-leaves call ([MediaServerClient.fetchPlayableDescendants] —
/// Plex's `/library/metadata/{id}/allLeaves`, Jellyfin's
/// `/Items?Recursive=true&IncludeItemTypes=Movie,Episode`). Avoids walking
/// show → seasons → episodes client-side, so large series come back in one
/// trip and aren't capped by any per-page Limit.
///
/// A failure of the underlying call propagates to the caller — both
/// `DownloadProvider.queueDownload` and the sync rule executor wrap their
/// invocations so the user-facing error surfaces / the rule run is rolled
/// back.
Future<void> collectEpisodesForShow(
MediaServerClient client,
String showRatingKey, {
required bool unwatchedOnly,
required List<MediaItem> out,
MediaItem? fallback,
}) {
return _collectPlayable(client, showRatingKey, unwatchedOnly: unwatchedOnly, out: out, fallback: fallback);
}
/// Collect every episode of a single season into [out] via the same
/// one-shot endpoint. On a season the leaves *are* the episodes, so the
/// shape matches the show case.
Future<void> collectEpisodesForSeason(
MediaServerClient client,
String seasonRatingKey, {
required bool unwatchedOnly,
required List<MediaItem> out,
MediaItem? fallback,
}) {
return _collectPlayable(client, seasonRatingKey, unwatchedOnly: unwatchedOnly, out: out, fallback: fallback);
}
/// Fetch just the first episode of a season without walking the entire season.
/// Use this for representative lookups and immediate "play first" actions.
Future<MediaItem?> fetchFirstEpisodeForSeason(MediaServerClient client, String seasonRatingKey) async {
final page = await client.fetchChildrenPage(seasonRatingKey, start: 0, size: 1);
for (final item in page.items) {
if (item.kind == MediaKind.episode) return item;
}
return null;
}
/// Prefer the first regular season over specials, falling back to the first
/// season row when a show only has specials or lacks season indexes.
int defaultPlaybackSeasonIndex(List<MediaItem> seasons) {
if (seasons.isEmpty) return 0;
final regularSeasonIndex = seasons.indexWhere((season) => season.kind == MediaKind.season && (season.index ?? 0) > 0);
if (regularSeasonIndex != -1) return regularSeasonIndex;
final firstSeasonIndex = seasons.indexWhere((season) => season.kind == MediaKind.season);
return firstSeasonIndex == -1 ? 0 : firstSeasonIndex;
}
MediaItem? defaultPlaybackSeason(List<MediaItem> seasons) {
if (seasons.isEmpty) return null;
final index = defaultPlaybackSeasonIndex(seasons);
if (index < 0 || index >= seasons.length) return null;
final season = seasons[index];
return season.kind == MediaKind.season ? season : null;
}
/// Find the season index matching an explicit navigation target or on-deck
/// episode, then fall back to [defaultPlaybackSeasonIndex].
int preferredSeasonIndex(List<MediaItem> seasons, {int? initialSeasonIndex, MediaItem? onDeckEpisode}) {
if (seasons.isEmpty) return 0;
if (initialSeasonIndex != null) {
final idx = seasons.indexWhere((season) => season.kind == MediaKind.season && season.index == initialSeasonIndex);
if (idx != -1) return idx;
}
if (onDeckEpisode != null) {
final parentId = onDeckEpisode.parentId;
if (parentId != null) {
final idx = seasons.indexWhere((season) => season.kind == MediaKind.season && season.id == parentId);
if (idx != -1) return idx;
}
final parentIndex = onDeckEpisode.parentIndex;
if (parentIndex != null) {
final idx = seasons.indexWhere((season) => season.kind == MediaKind.season && season.index == parentIndex);
if (idx != -1) return idx;
}
}
return defaultPlaybackSeasonIndex(seasons);
}
/// Fetch a page of direct season children and normalize the episode identity
/// fields detail rows depend on. Local/session progress stays layered in UI.
Future<LibraryPage<MediaItem>> fetchSeasonEpisodePage(
MediaServerClient client, {
required MediaItem show,
required MediaItem season,
required int start,
required int size,
}) async {
final page = await client.fetchChildrenPage(season.id, start: start, size: size);
return LibraryPage<MediaItem>(
items: normalizeSeasonEpisodes(page.items, show: show, season: season),
totalCount: page.totalCount,
offset: page.offset,
);
}
List<MediaItem> normalizeSeasonEpisodes(
List<MediaItem> episodes, {
required MediaItem show,
required MediaItem season,
}) {
return episodes
.map(
(episode) => _withFallbackLibrary(
episode.copyWith(
serverId: show.serverId ?? episode.serverId,
serverName: show.serverName ?? episode.serverName,
grandparentId: show.id,
grandparentTitle: show.title ?? episode.grandparentTitle,
parentId: episode.parentId ?? season.id,
parentIndex: episode.parentIndex ?? season.index,
),
season.libraryId != null ? season : show,
),
)
.toList();
}
Future<void> _collectPlayable(
MediaServerClient client,
String parentId, {
required bool unwatchedOnly,
required List<MediaItem> out,
MediaItem? fallback,
}) async {
final leaves = await client.fetchPlayableDescendants(parentId);
for (final ep in leaves) {
if (ep.kind != MediaKind.episode) continue;
if (unwatchedOnly && ep.isWatched && !ep.hasActiveProgress) continue;
out.add(_withFallbackLibrary(ep, fallback));
}
}
MediaItem _withFallbackLibrary(MediaItem item, MediaItem? fallback) {
if (fallback == null) return item;
final fallbackIsSeason = fallback.kind == MediaKind.season;
final fallbackIsShow = fallback.kind == MediaKind.show;
return item.copyWith(
serverId: item.serverId ?? fallback.serverId,
serverName: item.serverName ?? fallback.serverName,
libraryId: item.libraryId ?? fallback.libraryId,
libraryTitle: item.libraryTitle ?? fallback.libraryTitle,
parentId: item.parentId ?? (fallbackIsSeason ? fallback.id : null),
parentTitle: item.parentTitle ?? (fallbackIsSeason ? fallback.title : null),
grandparentId: item.grandparentId ?? _fallbackGrandparentId(fallback, isShow: fallbackIsShow),
grandparentTitle: item.grandparentTitle ?? _fallbackGrandparentTitle(fallback, isShow: fallbackIsShow),
);
}
String? _fallbackGrandparentId(MediaItem fallback, {required bool isShow}) {
if (isShow) return fallback.id;
return fallback.grandparentId ?? fallback.parentId;
}
String? _fallbackGrandparentTitle(MediaItem fallback, {required bool isShow}) {
if (isShow) return fallback.title;
return fallback.grandparentTitle ?? fallback.parentTitle;
}
+9 -3
View File
@@ -169,9 +169,15 @@ abstract class MediaServerClient {
/// show, tracks of an album, items of a collection. /// show, tracks of an album, items of a collection.
Future<List<MediaItem>> fetchChildren(String parentId); Future<List<MediaItem>> fetchChildren(String parentId);
/// Page through playable descendants of [parentId]. Used by large show / /// Page through direct children of [parentId]. Unlike
/// season detail views so episodes can render before the full list has /// [fetchPlayableDescendantsPage], this preserves container shape and is used
/// loaded. [fetchPlayableDescendants] remains the complete-list helper for /// for large season episode lists where the children endpoint is the correct
/// backend primitive.
Future<LibraryPage<MediaItem>> fetchChildrenPage(String parentId, {int? start, int? size, AbortController? abort});
/// Page through playable descendants of [parentId]. Used by flattened show /
/// season detail views so episodes can render before the full list has loaded.
/// [fetchPlayableDescendants] remains the complete-list helper for
/// playback/download/sync paths. /// playback/download/sync paths.
Future<LibraryPage<MediaItem>> fetchPlayableDescendantsPage( Future<LibraryPage<MediaItem>> fetchPlayableDescendantsPage(
String parentId, { String parentId, {
+115
View File
@@ -0,0 +1,115 @@
/// Immutable state for a list loaded in pages.
class PagedMediaListState<T> {
const PagedMediaListState({
this.items = const [],
this.totalCount = 0,
this.isInitialLoading = false,
this.isLoadingMore = false,
this.initialLoadFailed = false,
this.pageLoadFailed = false,
});
final List<T> items;
final int totalCount;
final bool isInitialLoading;
final bool isLoadingMore;
final bool initialLoadFailed;
final bool pageLoadFailed;
bool get hasMore => items.length < totalCount;
bool get hasItems => items.isNotEmpty;
PagedMediaListState<T> copyWith({
List<T>? items,
int? totalCount,
bool? isInitialLoading,
bool? isLoadingMore,
bool? initialLoadFailed,
bool? pageLoadFailed,
}) {
return PagedMediaListState<T>(
items: items ?? this.items,
totalCount: totalCount ?? this.totalCount,
isInitialLoading: isInitialLoading ?? this.isInitialLoading,
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
initialLoadFailed: initialLoadFailed ?? this.initialLoadFailed,
pageLoadFailed: pageLoadFailed ?? this.pageLoadFailed,
);
}
PagedMediaListState<T> startInitialLoad() {
return copyWith(isInitialLoading: true, isLoadingMore: false, initialLoadFailed: false, pageLoadFailed: false);
}
PagedMediaListState<T> completeInitialLoad(List<T> pageItems, int total) {
return copyWith(
items: List<T>.of(pageItems),
totalCount: total,
isInitialLoading: false,
isLoadingMore: false,
initialLoadFailed: false,
pageLoadFailed: false,
);
}
PagedMediaListState<T> failInitialLoad() {
return copyWith(isInitialLoading: false, isLoadingMore: false, initialLoadFailed: true);
}
PagedMediaListState<T> startLoadMore() {
return copyWith(isLoadingMore: true, pageLoadFailed: false);
}
PagedMediaListState<T> completeLoadMore({
required int expectedOffset,
required List<T> pageItems,
required int total,
}) {
if (items.length != expectedOffset) {
return copyWith(isLoadingMore: false);
}
final nextTotal = pageItems.isEmpty ? expectedOffset : total;
return copyWith(
items: <T>[...items, ...pageItems],
totalCount: nextTotal,
isLoadingMore: false,
pageLoadFailed: false,
);
}
PagedMediaListState<T> failLoadMore() {
return copyWith(isLoadingMore: false, pageLoadFailed: true);
}
PagedMediaListState<T> replaceItems(List<T> nextItems) {
final nextTotal = totalCount < nextItems.length ? nextItems.length : totalCount;
return copyWith(items: List<T>.of(nextItems), totalCount: nextTotal);
}
PagedMediaListState<T> removeWhere(bool Function(T item) test) {
var removed = 0;
final nextItems = <T>[];
for (final item in items) {
if (test(item)) {
removed++;
} else {
nextItems.add(item);
}
}
if (removed == 0) return this;
final decrementedTotal = totalCount - removed;
final nextTotal = decrementedTotal < nextItems.length ? nextItems.length : decrementedTotal;
return copyWith(items: nextItems, totalCount: nextTotal);
}
PagedMediaListState<R> mapItems<R>(R Function(T item) map) {
return PagedMediaListState<R>(
items: items.map(map).toList(),
totalCount: totalCount,
isInitialLoading: isInitialLoading,
isLoadingMore: isLoadingMore,
initialLoadFailed: initialLoadFailed,
pageLoadFailed: pageLoadFailed,
);
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ import '../media/media_server_client.dart';
import '../services/sync_rule_executor.dart'; import '../services/sync_rule_executor.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../utils/deletion_notifier.dart'; import '../utils/deletion_notifier.dart';
import '../utils/episode_collection.dart'; import '../media/episode_collection.dart';
import '../utils/global_key_utils.dart'; import '../utils/global_key_utils.dart';
import '../utils/watch_state_notifier.dart'; import '../utils/watch_state_notifier.dart';
import '../mixins/disposable_change_notifier_mixin.dart'; import '../mixins/disposable_change_notifier_mixin.dart';
File diff suppressed because it is too large Load Diff
@@ -523,6 +523,82 @@ mixin _JellyfinBrowseMethods on MediaServerCacheMixin {
return _mapItems(allRaw); return _mapItems(allRaw);
} }
@override
Future<LibraryPage<MediaItem>> fetchChildrenPage(
String parentId, {
int? start,
int? size,
AbortController? abort,
}) async {
final offset = start ?? 0;
final pageSize = size ?? _pagedListPageSize;
final seasonsKey = '/Shows/$parentId/Seasons?userId=${connection.userId}';
final childrenKey = '/Items?ParentId=$parentId&userId=${connection.userId}';
if (isOfflineMode) {
final cachedSeasons = await cache.get(ServerId(cacheServerId), seasonsKey);
if (cachedSeasons != null) {
final allSeasons = _mapItems(_itemsArray(cachedSeasons));
if (allSeasons.isNotEmpty) {
final safeOffset = offset.clamp(0, allSeasons.length).toInt();
final end = (safeOffset + pageSize).clamp(0, allSeasons.length).toInt();
return LibraryPage<MediaItem>(
items: allSeasons.sublist(safeOffset, end),
totalCount: allSeasons.length,
offset: offset,
);
}
}
final cached = await cache.get(ServerId(cacheServerId), childrenKey);
final all = cached == null ? const <MediaItem>[] : _mapItems(_itemsArray(cached));
final safeOffset = offset.clamp(0, all.length).toInt();
final end = (safeOffset + pageSize).clamp(0, all.length).toInt();
final pageItems = all.sublist(safeOffset, end);
return LibraryPage<MediaItem>(items: pageItems, totalCount: all.length, offset: offset);
}
try {
final seasons = await _http.get(
'/Shows/${_segment(parentId)}/Seasons',
queryParameters: {
'userId': connection.userId,
'StartIndex': offset.toString(),
'Limit': pageSize.toString(),
'EnableTotalRecordCount': 'true',
'Fields': _browseFields,
...jellyfinImageQueryParameters,
},
abort: abort,
);
if (seasons.statusCode == 200) {
final data = seasons.data;
final items = _itemsArray(data);
final rawTotal = data is Map<String, dynamic> ? data['TotalRecordCount'] : null;
if (items.isNotEmpty || (rawTotal is int && rawTotal > 0)) {
return _pagedMediaItems(data, offset: offset, requestedSize: pageSize);
}
}
} on MediaServerHttpException {
// Not a series — fall through to the generic ParentId query.
}
final response = await _http.get(
'/Items',
queryParameters: {
'userId': connection.userId,
'ParentId': parentId,
'StartIndex': offset.toString(),
'Limit': pageSize.toString(),
'EnableTotalRecordCount': 'true',
'Fields': _episodeRowFields,
...jellyfinImageQueryParameters,
},
abort: abort,
);
throwIfHttpError(response);
return _pagedMediaItems(response.data, offset: offset, requestedSize: pageSize);
}
/// Jellyfin folder browsing mirrors Jellyfin Web/Findroid/Swiftfin: query /// Jellyfin folder browsing mirrors Jellyfin Web/Findroid/Swiftfin: query
/// direct children of the library/folder with `Recursive=false`. This is /// direct children of the library/folder with `Recursive=false`. This is
/// distinct from [fetchLibraryContent], which intentionally recurses through /// distinct from [fetchLibraryContent], which intentionally recurses through
+83 -3
View File
@@ -1206,6 +1206,56 @@ class PlexClient
return all; return all;
} }
/// Walk every page of [path] and return a single synthesized response whose
/// `MediaContainer.Metadata` concatenates all pages. Lets a caller (and its
/// cache layer) treat a large, server-paginated collection as one complete
/// response while each network request stays small. Raw-response analog of
/// [_fetchAllPages]; errors propagate.
Future<MediaServerResponse> _getAllPagesResponse(
String path, {
Map<String, dynamic>? queryParameters,
AbortController? abort,
}) async {
MediaServerResponse? firstResponse;
Map<String, dynamic>? firstContainer;
final allMetadata = <dynamic>[];
var start = 0;
while (true) {
final response = await _getWithFailover(
path,
queryParameters: {...?queryParameters, ..._buildPaginationParams(start, _fetchAllPageSize)},
abort: abort,
);
final container = _getMediaContainer(response);
final metadata = container?['Metadata'];
final pageItems = metadata is List ? metadata : const [];
firstResponse ??= response;
firstContainer ??= container;
allMetadata.addAll(pageItems);
final total = _responseTotalSize(
response,
itemCount: pageItems.length,
start: start,
requestedSize: _fetchAllPageSize,
);
start += pageItems.length;
if (pageItems.isEmpty || start >= total) break;
}
return MediaServerResponse(
statusCode: firstResponse.statusCode,
data: {
'MediaContainer': {
...?firstContainer,
'Metadata': allMetadata,
'size': allMetadata.length,
'totalSize': allMetadata.length,
},
},
headers: firstResponse.headers,
requestUri: firstResponse.requestUri,
);
}
/// Set per-media language preferences (audio and subtitle) /// Set per-media language preferences (audio and subtitle)
/// For TV shows, use grandparentRatingKey to set preference for the entire series /// For TV shows, use grandparentRatingKey to set preference for the entire series
/// For movies, use the movie's ratingKey /// For movies, use the movie's ratingKey
@@ -1412,20 +1462,35 @@ class PlexClient
return result; return result;
} }
/// Get children of a metadata item (e.g., seasons for a show, episodes for a season) /// Get children of a metadata item (e.g., seasons for a show, episodes for a season).
/// Uses cache when offline or as fallback on network error /// Walks every page so large shows (many seasons) aren't truncated by a
/// server-forced container limit; uses cache when offline or as fallback on
/// network error. (Large *episode* lists load lazily via [fetchChildrenPage];
/// this full-fetch is for the seasons list and other complete-list callers.)
Future<List<PlexMetadataDto>> _getChildren(String ratingKey) async { Future<List<PlexMetadataDto>> _getChildren(String ratingKey) async {
final endpoint = '/library/metadata/$ratingKey/children'; final endpoint = '/library/metadata/$ratingKey/children';
return await fetchWithCacheFallback<List<PlexMetadataDto>>( return await fetchWithCacheFallback<List<PlexMetadataDto>>(
cacheKey: endpoint, cacheKey: endpoint,
networkCall: () => _http.get(endpoint, queryParameters: {'includeStreams': 1}), networkCall: () => _getAllPagesResponse(endpoint, queryParameters: {'includeStreams': 1}),
parseCache: (cachedData) => _parseMetadataListFromCachedResponse(cachedData), parseCache: (cachedData) => _parseMetadataListFromCachedResponse(cachedData),
parseResponse: (response) => _extractMetadataList(response), parseResponse: (response) => _extractMetadataList(response),
) ?? ) ??
[]; [];
} }
/// Page through direct children of a metadata item (e.g. episodes of a
/// season). This uses `/children`; playable descendant paging uses
/// `/grandchildren` and intentionally has different semantics.
Future<_LibraryContentResult> _getChildrenPage(String ratingKey, {int? start, int? size, AbortController? abort}) =>
_fetchPaginatedList(
'/library/metadata/$ratingKey/children',
start: start,
size: size,
abort: abort,
queryParameters: {'includeStreams': 1},
);
/// Page through playable episodes beneath a show or season. Uses /// Page through playable episodes beneath a show or season. Uses
/// `/grandchildren` rather than `/allLeaves` because the live server returns /// `/grandchildren` rather than `/allLeaves` because the live server returns
/// 0 items for `/allLeaves` on a season. /// 0 items for `/allLeaves` on a season.
@@ -3264,6 +3329,21 @@ class PlexClient
return children.map((m) => PlexMappers.mediaItem(m)).toList(); return children.map((m) => PlexMappers.mediaItem(m)).toList();
} }
@override
Future<LibraryPage<MediaItem>> fetchChildrenPage(
String parentId, {
int? start,
int? size,
AbortController? abort,
}) async {
final result = await _getChildrenPage(parentId, start: start, size: size, abort: abort);
return LibraryPage<MediaItem>(
items: result.items.map((m) => PlexMappers.mediaItem(m)).toList(),
totalCount: result.totalSize,
offset: start ?? 0,
);
}
@override @override
Future<List<MediaItem>> fetchPlayableDescendants(String parentId) async { Future<List<MediaItem>> fetchPlayableDescendants(String parentId) async {
final leaves = await _fetchAllPages( final leaves = await _fetchAllPages(
+1 -1
View File
@@ -8,7 +8,7 @@ import '../media/media_server_client.dart';
import '../models/download_models.dart'; import '../models/download_models.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../utils/content_utils.dart'; import '../utils/content_utils.dart';
import '../utils/episode_collection.dart'; import '../media/episode_collection.dart';
import '../utils/global_key_utils.dart'; import '../utils/global_key_utils.dart';
import 'download_manager_service.dart'; import 'download_manager_service.dart';
import 'multi_server_manager.dart'; import 'multi_server_manager.dart';
@@ -5,7 +5,7 @@ import '../../media/media_kind.dart';
import '../../media/media_server_client.dart'; import '../../media/media_server_client.dart';
import '../../models/trackers/tracker_context.dart'; import '../../models/trackers/tracker_context.dart';
import '../../utils/app_logger.dart'; import '../../utils/app_logger.dart';
import '../../utils/episode_collection.dart'; import '../../media/episode_collection.dart';
import 'anime_episode_progress_resolver.dart'; import 'anime_episode_progress_resolver.dart';
import 'anime_lists_mapping_store.dart'; import 'anime_lists_mapping_store.dart';
import 'anilist/anilist_tracker.dart'; import 'anilist/anilist_tracker.dart';
+1 -1
View File
@@ -8,7 +8,7 @@ import '../../media/media_server_client.dart';
import '../../models/trakt/trakt_ids.dart'; import '../../models/trakt/trakt_ids.dart';
import '../../models/trakt/trakt_scrobble_request.dart'; import '../../models/trakt/trakt_scrobble_request.dart';
import '../../utils/app_logger.dart'; import '../../utils/app_logger.dart';
import '../../utils/episode_collection.dart'; import '../../media/episode_collection.dart';
import '../../utils/watch_state_notifier.dart'; import '../../utils/watch_state_notifier.dart';
import '../multi_server_manager.dart'; import '../multi_server_manager.dart';
import '../settings_service.dart'; import '../settings_service.dart';
+4 -8
View File
@@ -4,6 +4,7 @@ import '../media/media_item.dart';
import '../media/media_kind.dart'; import '../media/media_kind.dart';
import '../media/media_server_client.dart'; import '../media/media_server_client.dart';
import '../media/media_version.dart'; import '../media/media_version.dart';
import '../media/episode_collection.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../utils/dialogs.dart'; import '../utils/dialogs.dart';
import '../utils/media_version_resolver.dart'; import '../utils/media_version_resolver.dart';
@@ -97,18 +98,13 @@ Future<List<MediaVersion>?> fetchRepresentativeVersions(MediaServerClient client
String? episodeRatingKey; String? episodeRatingKey;
if (metadata.kind == MediaKind.season) { if (metadata.kind == MediaKind.season) {
final episodes = await client.fetchChildren(metadata.id); final firstEpisode = await fetchFirstEpisodeForSeason(client, metadata.id);
final firstEpisode = episodes.where((e) => e.kind == MediaKind.episode).firstOrNull;
episodeRatingKey = firstEpisode?.id; episodeRatingKey = firstEpisode?.id;
} else if (metadata.kind == MediaKind.show) { } else if (metadata.kind == MediaKind.show) {
final seasons = await client.fetchChildren(metadata.id); final seasons = await client.fetchChildren(metadata.id);
// Skip Season 0 (Specials) as it may have different encoding final firstSeason = defaultPlaybackSeason(seasons);
final firstSeason =
seasons.where((s) => s.kind == MediaKind.season && (s.index ?? 0) > 0).firstOrNull ??
seasons.where((s) => s.kind == MediaKind.season).firstOrNull;
if (firstSeason != null) { if (firstSeason != null) {
final episodes = await client.fetchChildren(firstSeason.id); final firstEpisode = await fetchFirstEpisodeForSeason(client, firstSeason.id);
final firstEpisode = episodes.where((e) => e.kind == MediaKind.episode).firstOrNull;
episodeRatingKey = firstEpisode?.id; episodeRatingKey = firstEpisode?.id;
} }
} }
-78
View File
@@ -1,78 +0,0 @@
import '../media/media_item.dart';
import '../media/media_kind.dart';
import '../media/media_server_client.dart';
/// Collect every episode of a show into [out] using the backend's one-shot
/// recursive-leaves call ([MediaServerClient.fetchPlayableDescendants] —
/// Plex's `/library/metadata/{id}/allLeaves`, Jellyfin's
/// `/Items?Recursive=true&IncludeItemTypes=Movie,Episode`). Avoids walking
/// show → seasons → episodes client-side, so large series come back in one
/// trip and aren't capped by any per-page Limit.
///
/// A failure of the underlying call propagates to the caller — both
/// [DownloadProvider.queueDownload] and the sync rule executor wrap their
/// invocations so the user-facing error surfaces / the rule run is rolled
/// back.
Future<void> collectEpisodesForShow(
MediaServerClient client,
String showRatingKey, {
required bool unwatchedOnly,
required List<MediaItem> out,
MediaItem? fallback,
}) {
return _collectPlayable(client, showRatingKey, unwatchedOnly: unwatchedOnly, out: out, fallback: fallback);
}
/// Collect every episode of a single season into [out] via the same
/// one-shot endpoint. On a season the leaves *are* the episodes, so the
/// shape matches the show case.
Future<void> collectEpisodesForSeason(
MediaServerClient client,
String seasonRatingKey, {
required bool unwatchedOnly,
required List<MediaItem> out,
MediaItem? fallback,
}) {
return _collectPlayable(client, seasonRatingKey, unwatchedOnly: unwatchedOnly, out: out, fallback: fallback);
}
Future<void> _collectPlayable(
MediaServerClient client,
String parentId, {
required bool unwatchedOnly,
required List<MediaItem> out,
MediaItem? fallback,
}) async {
final leaves = await client.fetchPlayableDescendants(parentId);
for (final ep in leaves) {
if (ep.kind != MediaKind.episode) continue;
if (unwatchedOnly && ep.isWatched && !ep.hasActiveProgress) continue;
out.add(_withFallbackLibrary(ep, fallback));
}
}
MediaItem _withFallbackLibrary(MediaItem item, MediaItem? fallback) {
if (fallback == null) return item;
final fallbackIsSeason = fallback.kind == MediaKind.season;
final fallbackIsShow = fallback.kind == MediaKind.show;
return item.copyWith(
serverId: item.serverId ?? fallback.serverId,
serverName: item.serverName ?? fallback.serverName,
libraryId: item.libraryId ?? fallback.libraryId,
libraryTitle: item.libraryTitle ?? fallback.libraryTitle,
parentId: item.parentId ?? (fallbackIsSeason ? fallback.id : null),
parentTitle: item.parentTitle ?? (fallbackIsSeason ? fallback.title : null),
grandparentId: item.grandparentId ?? _fallbackGrandparentId(fallback, isShow: fallbackIsShow),
grandparentTitle: item.grandparentTitle ?? _fallbackGrandparentTitle(fallback, isShow: fallbackIsShow),
);
}
String? _fallbackGrandparentId(MediaItem fallback, {required bool isShow}) {
if (isShow) return fallback.id;
return fallback.grandparentId ?? fallback.parentId;
}
String? _fallbackGrandparentTitle(MediaItem fallback, {required bool isShow}) {
if (isShow) return fallback.title;
return fallback.grandparentTitle ?? fallback.parentTitle;
}
+187 -38
View File
@@ -137,7 +137,11 @@ class TvBrowseRailLayout {
final railEdgePadding = focusExtra + (12 * scale); final railEdgePadding = focusExtra + (12 * scale);
final itemGap = fullCardLayout ? fullCardItemGapForScale(scale) : itemGapForScale(scale); final itemGap = fullCardLayout ? fullCardItemGapForScale(scale) : itemGapForScale(scale);
final isPersonHub = TvBrowseRailLayout.isPersonHub(hub); final isPersonHub = TvBrowseRailLayout.isPersonHub(hub);
final hasWide = !isPersonHub && hub.items.any((item) => item.usesWideAspectRatio(episodePosterMode)); final emptyEpisodeThumbnailHub =
hub.items.isEmpty && hub.type == 'episode' && episodePosterMode == EpisodePosterMode.episodeThumbnail;
final hasWide =
!isPersonHub &&
(emptyEpisodeThumbnailHub || hub.items.any((item) => item.usesWideAspectRatio(episodePosterMode)));
final hasTall = !isPersonHub && hub.items.any((item) => !item.usesWideAspectRatio(episodePosterMode)); final hasTall = !isPersonHub && hub.items.any((item) => !item.usesWideAspectRatio(episodePosterMode));
final isMixedHub = hasWide && hasTall; final isMixedHub = hasWide && hasTall;
final useWideLayout = hasWide && (!hasTall || episodePosterMode == EpisodePosterMode.episodeThumbnail); final useWideLayout = hasWide && (!hasTall || episodePosterMode == EpisodePosterMode.episodeThumbnail);
@@ -205,9 +209,11 @@ class TvBrowseRailLayout {
required TvBrowseRailLayoutMetrics metrics, required TvBrowseRailLayoutMetrics metrics,
required double viewportWidth, required double viewportWidth,
required double scale, required double scale,
bool? hasTrailing,
}) { }) {
final showTrailing = hasTrailing ?? hub.more;
final itemContentWidth = hub.items.length * (metrics.cardWidth + metrics.itemGap); final itemContentWidth = hub.items.length * (metrics.cardWidth + metrics.itemGap);
final moreContentWidth = hub.more ? viewAllItemWidthForScale(scale) + metrics.itemGap : 0.0; final moreContentWidth = showTrailing ? viewAllItemWidthForScale(scale) + metrics.itemGap : 0.0;
final contentWidth = (metrics.railEdgePadding * 2) + itemContentWidth + moreContentWidth; final contentWidth = (metrics.railEdgePadding * 2) + itemContentWidth + moreContentWidth;
return (contentWidth - viewportWidth).clamp(0.0, double.infinity).toDouble(); return (contentWidth - viewportWidth).clamp(0.0, double.infinity).toDouble();
} }
@@ -217,8 +223,10 @@ class TvBrowseRailLayout {
required int index, required int index,
required TvBrowseRailLayoutMetrics metrics, required TvBrowseRailLayoutMetrics metrics,
required double scale, required double scale,
bool? hasTrailing,
}) { }) {
if (index == hub.items.length && hub.more) return viewAllItemWidthForScale(scale) + metrics.itemGap; final showTrailing = hasTrailing ?? hub.more;
if (index == hub.items.length && showTrailing) return viewAllItemWidthForScale(scale) + metrics.itemGap;
return metrics.cardWidth + metrics.itemGap; return metrics.cardWidth + metrics.itemGap;
} }
@@ -229,15 +237,23 @@ class TvBrowseRailLayout {
required double viewportWidth, required double viewportWidth,
required double maxScrollExtent, required double maxScrollExtent,
required double scale, required double scale,
bool? hasTrailing,
}) { }) {
final totalCount = hub.items.length + (hub.more ? 1 : 0); final showTrailing = hasTrailing ?? hub.more;
final totalCount = hub.items.length + (showTrailing ? 1 : 0);
if (totalCount == 0) return 0; if (totalCount == 0) return 0;
final clampedIndex = index.clamp(0, totalCount - 1).toInt(); final clampedIndex = index.clamp(0, totalCount - 1).toInt();
final normalItemExtent = metrics.cardWidth + metrics.itemGap; final normalItemExtent = metrics.cardWidth + metrics.itemGap;
final normalItemsBefore = clampedIndex < hub.items.length ? clampedIndex : hub.items.length; final normalItemsBefore = clampedIndex < hub.items.length ? clampedIndex : hub.items.length;
final leadingOffset = metrics.railEdgePadding + (normalItemsBefore * normalItemExtent); final leadingOffset = metrics.railEdgePadding + (normalItemsBefore * normalItemExtent);
final targetExtent = itemExtentForIndex(hub: hub, index: clampedIndex, metrics: metrics, scale: scale); final targetExtent = itemExtentForIndex(
hub: hub,
index: clampedIndex,
metrics: metrics,
scale: scale,
hasTrailing: showTrailing,
);
final targetCenter = leadingOffset + (targetExtent / 2); final targetCenter = leadingOffset + (targetExtent / 2);
return (targetCenter - (viewportWidth / 2)).clamp(0.0, maxScrollExtent).toDouble(); return (targetCenter - (viewportWidth / 2)).clamp(0.0, maxScrollExtent).toDouble();
} }
@@ -280,6 +296,9 @@ class TvBrowseRailLayout {
} }
} }
/// What [TvBrowseRail] renders in a hub's trailing slot (after the last item).
enum TvRailTrailing { none, loading, error, viewAll }
class TvBrowseRail extends StatefulWidget { class TvBrowseRail extends StatefulWidget {
final List<MediaHub> hubs; final List<MediaHub> hubs;
final IconData Function(MediaHub hub, int index) iconForHub; final IconData Function(MediaHub hub, int index) iconForHub;
@@ -289,6 +308,13 @@ class TvBrowseRail extends StatefulWidget {
final VoidCallback? onRemoveFromContinueWatching; final VoidCallback? onRemoveFromContinueWatching;
final bool Function(MediaHub hub)? isContinueWatchingHub; final bool Function(MediaHub hub)? isContinueWatchingHub;
final Future<List<MediaItem>> Function(MediaHub hub)? loadMoreItems; final Future<List<MediaItem>> Function(MediaHub hub)? loadMoreItems;
/// Optional per-hub trailing-slot state (loading/error/viewAll). When null the
/// rail keeps the legacy "[MediaHub.more] → View All" behavior.
final TvRailTrailing Function(MediaHub hub)? trailingForHub;
/// Invoked when the user activates a hub's trailing retry tile.
final void Function(MediaHub hub)? onRetryHub;
final void Function(MediaHub hub, int index)? onActiveHubChanged; final void Function(MediaHub hub, int index)? onActiveHubChanged;
final VoidCallback? onNavigateUp; final VoidCallback? onNavigateUp;
final VoidCallback? onNavigateToSidebar; final VoidCallback? onNavigateToSidebar;
@@ -313,6 +339,8 @@ class TvBrowseRail extends StatefulWidget {
this.onRemoveFromContinueWatching, this.onRemoveFromContinueWatching,
this.isContinueWatchingHub, this.isContinueWatchingHub,
this.loadMoreItems, this.loadMoreItems,
this.trailingForHub,
this.onRetryHub,
this.onActiveHubChanged, this.onActiveHubChanged,
this.onNavigateUp, this.onNavigateUp,
this.onNavigateToSidebar, this.onNavigateToSidebar,
@@ -347,6 +375,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
final Map<String, GlobalKey<MediaCardState>> _mediaCardKeys = {}; final Map<String, GlobalKey<MediaCardState>> _mediaCardKeys = {};
final Map<String, TvBrowseRailLayoutMetrics> _metricsByHub = {}; final Map<String, TvBrowseRailLayoutMetrics> _metricsByHub = {};
final Map<String, double> _scaleByHub = {}; final Map<String, double> _scaleByHub = {};
final Map<String, TvRailTrailing> _lastTrailingByHubId = {};
int _hubIndex = 0; int _hubIndex = 0;
int _itemIndex = 0; int _itemIndex = 0;
@@ -382,6 +411,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
_focusNode.addListener(_handleFocusChange); _focusNode.addListener(_handleFocusChange);
_selectInitialHubIfPossible(); _selectInitialHubIfPossible();
final selectedInitialItem = _selectInitialItemIfPossible(); final selectedInitialItem = _selectInitialItemIfPossible();
_rememberTrailingStates();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || widget.hubs.isEmpty) return; if (!mounted || widget.hubs.isEmpty) return;
if (selectedInitialItem) _scrollToItem(animate: false); if (selectedInitialItem) _scrollToItem(animate: false);
@@ -395,11 +425,13 @@ class TvBrowseRailState extends State<TvBrowseRail> {
@override @override
void didUpdateWidget(covariant TvBrowseRail oldWidget) { void didUpdateWidget(covariant TvBrowseRail oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
final hubStateChanged = !_hasSameHubState(oldWidget.hubs, widget.hubs); final trailingStateChanged = _hasTrailingStateChanged(widget.hubs);
final hubStateChanged = trailingStateChanged || !_hasSameHubState(oldWidget.hubs, widget.hubs);
final initialSelectionChanged = final initialSelectionChanged =
oldWidget.initialHubId != widget.initialHubId || oldWidget.initialItemId != widget.initialItemId; oldWidget.initialHubId != widget.initialHubId || oldWidget.initialItemId != widget.initialItemId;
if (!hubStateChanged && !initialSelectionChanged) { if (!hubStateChanged && !initialSelectionChanged) {
_rememberTrailingStates();
if (!oldWidget.autofocus && widget.autofocus) { if (!oldWidget.autofocus && widget.autofocus) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _focusNode.requestFocus(); if (mounted) _focusNode.requestFocus();
@@ -415,6 +447,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
if (widget.hubs.isEmpty) { if (widget.hubs.isEmpty) {
_hubIndex = 0; _hubIndex = 0;
_itemIndex = 0; _itemIndex = 0;
_rememberTrailingStates();
return; return;
} }
@@ -435,11 +468,20 @@ class TvBrowseRailState extends State<TvBrowseRail> {
_itemIndex = _itemIndex.clamp(0, _totalItemCount(hub) == 0 ? 0 : _totalItemCount(hub) - 1); _itemIndex = _itemIndex.clamp(0, _totalItemCount(hub) == 0 ? 0 : _totalItemCount(hub) - 1);
final selectedInitialItem = _selectInitialItemIfPossible(); final selectedInitialItem = _selectInitialItemIfPossible();
final activeHubChanged = oldActiveHubId != _activeHub?.id; final activeHubChanged = oldActiveHubId != _activeHub?.id;
final shouldAlignActiveHub = selectedInitialHub || activeHubChanged || !_hasUserChangedHub; final activeHubStateChanged =
_hubStateChanged(oldWidget.hubs, widget.hubs, _hubIndex) ||
(_activeHub != null && _trailingStateChanged(_activeHub!));
final shouldJumpAlignActiveHub = selectedInitialHub || activeHubChanged || !_hasUserChangedHub;
final shouldAnimateAlignActiveHub = !shouldJumpAlignActiveHub && activeHubStateChanged;
_rememberTrailingStates();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return; if (!mounted) return;
if (selectedInitialItem) _scrollToItem(animate: false); if (selectedInitialItem) _scrollToItem(animate: false);
if (shouldAlignActiveHub) _scrollActiveHubToTop(animate: false); if (shouldJumpAlignActiveHub) {
_scrollActiveHubToTop(animate: false);
} else if (shouldAnimateAlignActiveHub) {
_scrollActiveHubToTop();
}
if (!oldWidget.autofocus && widget.autofocus) _focusNode.requestFocus(); if (!oldWidget.autofocus && widget.autofocus) _focusNode.requestFocus();
if (activeHubChanged) _notifyActiveHubChanged(); if (activeHubChanged) _notifyActiveHubChanged();
_notifyFocusedItem(); _notifyFocusedItem();
@@ -449,18 +491,42 @@ class TvBrowseRailState extends State<TvBrowseRail> {
bool _hasSameHubState(List<MediaHub> oldHubs, List<MediaHub> newHubs) { bool _hasSameHubState(List<MediaHub> oldHubs, List<MediaHub> newHubs) {
if (oldHubs.length != newHubs.length) return false; if (oldHubs.length != newHubs.length) return false;
for (var i = 0; i < oldHubs.length; i++) { for (var i = 0; i < oldHubs.length; i++) {
final oldHub = oldHubs[i]; if (_hubStateChanged(oldHubs, newHubs, i)) return false;
final newHub = newHubs[i];
if (oldHub.id != newHub.id || oldHub.more != newHub.more || oldHub.items.length != newHub.items.length) {
return false;
}
for (var j = 0; j < oldHub.items.length; j++) {
if (oldHub.items[j].globalKey != newHub.items[j].globalKey) return false;
}
} }
return true; return true;
} }
bool _hubStateChanged(List<MediaHub> oldHubs, List<MediaHub> newHubs, int index) {
if (index < 0 || index >= oldHubs.length || index >= newHubs.length) return true;
final oldHub = oldHubs[index];
final newHub = newHubs[index];
if (oldHub.id != newHub.id || oldHub.more != newHub.more || oldHub.items.length != newHub.items.length) {
return true;
}
for (var j = 0; j < oldHub.items.length; j++) {
if (oldHub.items[j].globalKey != newHub.items[j].globalKey) return true;
}
return false;
}
bool _hasTrailingStateChanged(List<MediaHub> hubs) {
for (final hub in hubs) {
if (_trailingStateChanged(hub)) return true;
}
return false;
}
bool _trailingStateChanged(MediaHub hub) {
final previous = _lastTrailingByHubId[hub.id];
return previous != null && previous != _trailingFor(hub);
}
void _rememberTrailingStates() {
_lastTrailingByHubId
..clear()
..addEntries(widget.hubs.map((hub) => MapEntry(hub.id, _trailingFor(hub))));
}
@override @override
void dispose() { void dispose() {
_longPressTimer?.cancel(); _longPressTimer?.cancel();
@@ -492,7 +558,9 @@ class TvBrowseRailState extends State<TvBrowseRail> {
_suppressSelectUntilKeyUp = false; _suppressSelectUntilKeyUp = false;
} }
int _totalItemCount(MediaHub hub) => hub.items.length + (hub.more ? 1 : 0); bool _hasTrailingFor(MediaHub hub) => _trailingFor(hub) != TvRailTrailing.none;
int _totalItemCount(MediaHub hub) => hub.items.length + (_hasTrailingFor(hub) ? 1 : 0);
bool _isPersonHub(MediaHub hub) => TvBrowseRailLayout.isPersonHub(hub); bool _isPersonHub(MediaHub hub) => TvBrowseRailLayout.isPersonHub(hub);
@@ -735,6 +803,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
viewportWidth: viewportWidth, viewportWidth: viewportWidth,
maxScrollExtent: maxScrollExtent, maxScrollExtent: maxScrollExtent,
scale: scale, scale: scale,
hasTrailing: _hasTrailingFor(hub),
); );
final distance = (position.pixels - target).abs(); final distance = (position.pixels - target).abs();
@@ -766,6 +835,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
metrics: metrics, metrics: metrics,
viewportWidth: viewportWidth, viewportWidth: viewportWidth,
scale: scale, scale: scale,
hasTrailing: _hasTrailingFor(hub),
); );
final initialScrollOffset = TvBrowseRailLayout.scrollOffsetForIndex( final initialScrollOffset = TvBrowseRailLayout.scrollOffsetForIndex(
hub: hub, hub: hub,
@@ -774,6 +844,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
viewportWidth: viewportWidth, viewportWidth: viewportWidth,
maxScrollExtent: maxScrollExtent, maxScrollExtent: maxScrollExtent,
scale: scale, scale: scale,
hasTrailing: _hasTrailingFor(hub),
); );
return ScrollController(initialScrollOffset: initialScrollOffset); return ScrollController(initialScrollOffset: initialScrollOffset);
}); });
@@ -793,8 +864,17 @@ class TvBrowseRailState extends State<TvBrowseRail> {
Future<void> _activateCurrentItem() async { Future<void> _activateCurrentItem() async {
final hub = _activeHub; final hub = _activeHub;
if (hub == null) return; if (hub == null) return;
if (_itemIndex == hub.items.length && hub.more) { if (_itemIndex == hub.items.length && _hasTrailingFor(hub)) {
_navigateToHubDetail(hub); switch (_trailingFor(hub)) {
case TvRailTrailing.error:
widget.onRetryHub?.call(hub);
break;
case TvRailTrailing.loading:
break; // no-op while the page loads
case TvRailTrailing.viewAll:
case TvRailTrailing.none:
_navigateToHubDetail(hub);
}
return; return;
} }
if (_itemIndex >= hub.items.length) return; if (_itemIndex >= hub.items.length) return;
@@ -1055,7 +1135,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
), ),
), ),
), ),
if (hub.more) ...[ if (_trailingFor(hub) == TvRailTrailing.viewAll) ...[
SizedBox(width: 8 * scale), SizedBox(width: 8 * scale),
AppIcon(Symbols.chevron_right_rounded, fill: 1, size: 20 * scale, color: iconColor), AppIcon(Symbols.chevron_right_rounded, fill: 1, size: 20 * scale, color: iconColor),
SizedBox(width: 30 * scale), SizedBox(width: 30 * scale),
@@ -1108,8 +1188,13 @@ class TvBrowseRailState extends State<TvBrowseRail> {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
clipBehavior: Clip.none, clipBehavior: Clip.none,
padding: .fromLTRB(metrics.railEdgePadding, 2 * scale, metrics.railEdgePadding, 6 * scale), padding: .fromLTRB(metrics.railEdgePadding, 2 * scale, metrics.railEdgePadding, 6 * scale),
itemExtentBuilder: (itemIndex, _) => itemExtentBuilder: (itemIndex, _) => TvBrowseRailLayout.itemExtentForIndex(
TvBrowseRailLayout.itemExtentForIndex(hub: hub, index: itemIndex, metrics: metrics, scale: scale), hub: hub,
index: itemIndex,
metrics: metrics,
scale: scale,
hasTrailing: _hasTrailingFor(hub),
),
itemCount: totalCount, itemCount: totalCount,
itemBuilder: (context, itemIndex) { itemBuilder: (context, itemIndex) {
final isFocused = hasFocus && isActiveHub && itemIndex == _itemIndex; final isFocused = hasFocus && isActiveHub && itemIndex == _itemIndex;
@@ -1118,15 +1203,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
padding: .only(right: metrics.itemGap), padding: .only(right: metrics.itemGap),
child: Align( child: Align(
alignment: .centerLeft, alignment: .centerLeft,
child: _buildViewAllButton( child: _buildTrailingSlot(context, hub, hubIndex, itemIndex, isFocused: isFocused, scale: scale),
context,
isFocused: isFocused,
scale: scale,
onTap: () {
_selectHubItem(hub, hubIndex, itemIndex);
_navigateToHubDetail(hub);
},
),
), ),
); );
} }
@@ -1316,11 +1393,88 @@ class TvBrowseRailState extends State<TvBrowseRail> {
); );
} }
/// Resolve the trailing-slot state for [hub], defaulting to the legacy
/// "[MediaHub.more] → View All" behavior when no resolver was supplied.
TvRailTrailing _trailingFor(MediaHub hub) =>
widget.trailingForHub?.call(hub) ?? (hub.more ? TvRailTrailing.viewAll : TvRailTrailing.none);
Widget _buildTrailingSlot(
BuildContext context,
MediaHub hub,
int hubIndex,
int itemIndex, {
required bool isFocused,
required double scale,
}) {
switch (_trailingFor(hub)) {
case TvRailTrailing.loading:
return _buildTrailingSpinner(context, isFocused: isFocused, scale: scale);
case TvRailTrailing.error:
return _buildViewAllButton(
context,
isFocused: isFocused,
scale: scale,
label: t.common.retry,
icon: Symbols.refresh_rounded,
onTap: () {
_selectHubItem(hub, hubIndex, itemIndex);
widget.onRetryHub?.call(hub);
},
);
case TvRailTrailing.viewAll:
return _buildViewAllButton(
context,
isFocused: isFocused,
scale: scale,
onTap: () {
_selectHubItem(hub, hubIndex, itemIndex);
_navigateToHubDetail(hub);
},
);
case TvRailTrailing.none:
return const SizedBox.shrink();
}
}
Widget _buildTrailingSpinner(BuildContext context, {required bool isFocused, required double scale}) {
final theme = Theme.of(context);
final duration = FocusTheme.getAnimationDuration(context);
final width = TvBrowseRailLayout.viewAllItemWidthForScale(scale);
final height = TvBrowseRailLayout.viewAllPillHeightForScale(scale);
final foreground = isFocused ? theme.colorScheme.primary : theme.colorScheme.onSurface.withValues(alpha: 0.78);
final background = isFocused
? theme.colorScheme.primary.withValues(alpha: 0.2)
: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.18);
return AnimatedScale(
scale: isFocused ? 1.04 : 1.0,
duration: duration,
curve: Curves.easeOutCubic,
child: AnimatedContainer(
duration: duration,
curve: Curves.easeOutCubic,
width: width,
height: height,
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(height / 2),
boxShadow: isFocused
? [BoxShadow(color: theme.colorScheme.primary.withValues(alpha: 0.2), blurRadius: 18, spreadRadius: 1)]
: null,
),
child: Center(
child: SizedBox(width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2, color: foreground)),
),
),
);
}
Widget _buildViewAllButton( Widget _buildViewAllButton(
BuildContext context, { BuildContext context, {
required bool isFocused, required bool isFocused,
required double scale, required double scale,
required VoidCallback onTap, required VoidCallback onTap,
String? label,
IconData icon = Symbols.arrow_forward_rounded,
}) { }) {
final theme = Theme.of(context); final theme = Theme.of(context);
final duration = FocusTheme.getAnimationDuration(context); final duration = FocusTheme.getAnimationDuration(context);
@@ -1362,7 +1516,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
children: [ children: [
Flexible( Flexible(
child: Text( child: Text(
t.common.viewAll, label ?? t.common.viewAll,
maxLines: 1, maxLines: 1,
overflow: .ellipsis, overflow: .ellipsis,
style: TextStyle( style: TextStyle(
@@ -1374,12 +1528,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
), ),
), ),
SizedBox(width: (5 * scale).clamp(4, 7).toDouble()), SizedBox(width: (5 * scale).clamp(4, 7).toDouble()),
AppIcon( AppIcon(icon, fill: 1, size: (18 * scale).clamp(16, 22).toDouble(), color: foreground),
Symbols.arrow_forward_rounded,
fill: 1,
size: (18 * scale).clamp(16, 22).toDouble(),
color: foreground,
),
], ],
), ),
), ),