feat: download options picker for series/seasons

This commit is contained in:
edde746
2026-04-07 22:59:35 +02:00
parent c034f9b66e
commit 8e7431e7e5
36 changed files with 447 additions and 44 deletions
+6 -1
View File
@@ -733,7 +733,12 @@
"pauseAll": "Pause alle",
"resumeAll": "Genoptag alle",
"deleteAll": "Slet alle",
"selectVersion": "Vælg version"
"selectVersion": "Vælg version",
"allEpisodes": "Alle episoder",
"unwatchedOnly": "Kun usete",
"nextNUnwatched": "Næste ${count} usete",
"customAmount": "Angiv antal...",
"howManyEpisodes": "Hvor mange episoder?"
},
"shaders": {
"title": "Shadere",
+6 -1
View File
@@ -630,7 +630,12 @@
"pauseAll": "Alle pausieren",
"resumeAll": "Alle fortsetzen",
"deleteAll": "Alle löschen",
"selectVersion": "Version auswählen"
"selectVersion": "Version auswählen",
"allEpisodes": "Alle Episoden",
"unwatchedOnly": "Nur ungesehene",
"nextNUnwatched": "Nächste ${count} ungesehene",
"customAmount": "Eigene Anzahl...",
"howManyEpisodes": "Wie viele Episoden?"
},
"playlists": {
"title": "Wiedergabelisten",
+6 -1
View File
@@ -733,7 +733,12 @@
"pauseAll": "Pause all",
"resumeAll": "Resume all",
"deleteAll": "Delete all",
"selectVersion": "Select Version"
"selectVersion": "Select Version",
"allEpisodes": "All episodes",
"unwatchedOnly": "Unwatched only",
"nextNUnwatched": "Next ${count} unwatched",
"customAmount": "Custom amount...",
"howManyEpisodes": "How many episodes?"
},
"shaders": {
"title": "Shaders",
+6 -1
View File
@@ -733,7 +733,12 @@
"pauseAll": "Pausar todo",
"resumeAll": "Reanudar todo",
"deleteAll": "Eliminar todo",
"selectVersion": "Seleccionar versión"
"selectVersion": "Seleccionar versión",
"allEpisodes": "Todos los episodios",
"unwatchedOnly": "Solo no vistos",
"nextNUnwatched": "Próximos ${count} no vistos",
"customAmount": "Cantidad personalizada...",
"howManyEpisodes": "¿Cuántos episodios?"
},
"shaders": {
"title": "Shaders",
+6 -1
View File
@@ -733,7 +733,12 @@
"pauseAll": "Tout mettre en pause",
"resumeAll": "Tout reprendre",
"deleteAll": "Tout supprimer",
"selectVersion": "Sélectionner la version"
"selectVersion": "Sélectionner la version",
"allEpisodes": "Tous les épisodes",
"unwatchedOnly": "Non vus uniquement",
"nextNUnwatched": "${count} prochains non vus",
"customAmount": "Quantité personnalisée...",
"howManyEpisodes": "Combien d'épisodes ?"
},
"shaders": {
"title": "Shaders",
+6 -1
View File
@@ -630,7 +630,12 @@
"pauseAll": "Metti tutto in pausa",
"resumeAll": "Riprendi tutto",
"deleteAll": "Elimina tutto",
"selectVersion": "Seleziona versione"
"selectVersion": "Seleziona versione",
"allEpisodes": "Tutti gli episodi",
"unwatchedOnly": "Solo non visti",
"nextNUnwatched": "Prossimi ${count} non visti",
"customAmount": "Quantità personalizzata...",
"howManyEpisodes": "Quanti episodi?"
},
"playlists": {
"title": "Playlist",
+6 -1
View File
@@ -733,7 +733,12 @@
"pauseAll": "すべて一時停止",
"resumeAll": "すべて再開",
"deleteAll": "すべて削除",
"selectVersion": "バージョンを選択"
"selectVersion": "バージョンを選択",
"allEpisodes": "すべてのエピソード",
"unwatchedOnly": "未視聴のみ",
"nextNUnwatched": "次の${count}件の未視聴",
"customAmount": "数を指定...",
"howManyEpisodes": "何エピソード?"
},
"shaders": {
"title": "シェーダー",
+6 -1
View File
@@ -733,7 +733,12 @@
"pauseAll": "모두 일시정지",
"resumeAll": "모두 재개",
"deleteAll": "모두 삭제",
"selectVersion": "버전 선택"
"selectVersion": "버전 선택",
"allEpisodes": "모든 에피소드",
"unwatchedOnly": "시청하지 않은 것만",
"nextNUnwatched": "다음 ${count}개 미시청",
"customAmount": "직접 입력...",
"howManyEpisodes": "몇 개의 에피소드?"
},
"shaders": {
"title": "셰이더",
+6 -1
View File
@@ -733,7 +733,12 @@
"pauseAll": "Pause alle",
"resumeAll": "Gjenoppta alle",
"deleteAll": "Slett alle",
"selectVersion": "Velg versjon"
"selectVersion": "Velg versjon",
"allEpisodes": "Alle episoder",
"unwatchedOnly": "Kun usette",
"nextNUnwatched": "Neste ${count} usette",
"customAmount": "Egendefinert antall...",
"howManyEpisodes": "Hvor mange episoder?"
},
"shaders": {
"title": "Shadere",
+6 -1
View File
@@ -630,7 +630,12 @@
"pauseAll": "Alles pauzeren",
"resumeAll": "Alles hervatten",
"deleteAll": "Alles verwijderen",
"selectVersion": "Versie selecteren"
"selectVersion": "Versie selecteren",
"allEpisodes": "Alle afleveringen",
"unwatchedOnly": "Alleen onbekeken",
"nextNUnwatched": "Volgende ${count} onbekeken",
"customAmount": "Aangepast aantal...",
"howManyEpisodes": "Hoeveel afleveringen?"
},
"playlists": {
"title": "Afspeellijsten",
+6 -1
View File
@@ -733,7 +733,12 @@
"pauseAll": "Wstrzymaj wszystko",
"resumeAll": "Wznów wszystko",
"deleteAll": "Usuń wszystko",
"selectVersion": "Wybierz wersję"
"selectVersion": "Wybierz wersję",
"allEpisodes": "Wszystkie odcinki",
"unwatchedOnly": "Tylko nieobejrzane",
"nextNUnwatched": "Następne ${count} nieobejrzanych",
"customAmount": "Własna ilość...",
"howManyEpisodes": "Ile odcinków?"
},
"shaders": {
"title": "Shadery",
+6 -1
View File
@@ -733,7 +733,12 @@
"pauseAll": "Pausar todos",
"resumeAll": "Retomar todos",
"deleteAll": "Excluir todos",
"selectVersion": "Selecionar versão"
"selectVersion": "Selecionar versão",
"allEpisodes": "Todos os episódios",
"unwatchedOnly": "Apenas não assistidos",
"nextNUnwatched": "Próximos ${count} não assistidos",
"customAmount": "Quantidade personalizada...",
"howManyEpisodes": "Quantos episódios?"
},
"shaders": {
"title": "Shaders",
+6 -1
View File
@@ -733,7 +733,12 @@
"pauseAll": "Приостановить все",
"resumeAll": "Возобновить все",
"deleteAll": "Удалить все",
"selectVersion": "Выбрать версию"
"selectVersion": "Выбрать версию",
"allEpisodes": "Все эпизоды",
"unwatchedOnly": "Только непросмотренные",
"nextNUnwatched": "Следующие ${count} непросмотренных",
"customAmount": "Указать количество...",
"howManyEpisodes": "Сколько эпизодов?"
},
"shaders": {
"title": "Шейдеры",
+2 -2
View File
@@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 15
/// Strings: 12750 (850 per locale)
/// Strings: 12825 (855 per locale)
///
/// Built on 2026-04-06 at 14:44 UTC
/// Built on 2026-04-07 at 18:27 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
+10
View File
@@ -994,6 +994,11 @@ class _TranslationsDownloadsDa implements TranslationsDownloadsEn {
@override String get resumeAll => 'Genoptag alle';
@override String get deleteAll => 'Slet alle';
@override String get selectVersion => 'Vælg version';
@override String get allEpisodes => 'Alle episoder';
@override String get unwatchedOnly => 'Kun usete';
@override String nextNUnwatched({required Object count}) => 'Næste ${count} usete';
@override String get customAmount => 'Angiv antal...';
@override String get howManyEpisodes => 'Hvor mange episoder?';
}
// Path: shaders
@@ -2013,6 +2018,11 @@ extension on TranslationsDa {
'downloads.resumeAll' => 'Genoptag alle',
'downloads.deleteAll' => 'Slet alle',
'downloads.selectVersion' => 'Vælg version',
'downloads.allEpisodes' => 'Alle episoder',
'downloads.unwatchedOnly' => 'Kun usete',
'downloads.nextNUnwatched' => ({required Object count}) => 'Næste ${count} usete',
'downloads.customAmount' => 'Angiv antal...',
'downloads.howManyEpisodes' => 'Hvor mange episoder?',
'shaders.title' => 'Shadere',
'shaders.noShaderDescription' => 'Ingen videoforbedring',
'shaders.nvscalerDescription' => 'NVIDIA-billedskalering for skarpere video',
+10
View File
@@ -870,6 +870,11 @@ class _TranslationsDownloadsDe implements TranslationsDownloadsEn {
@override String get resumeAll => 'Alle fortsetzen';
@override String get deleteAll => 'Alle löschen';
@override String get selectVersion => 'Version auswählen';
@override String get allEpisodes => 'Alle Episoden';
@override String get unwatchedOnly => 'Nur ungesehene';
@override String nextNUnwatched({required Object count}) => 'Nächste ${count} ungesehene';
@override String get customAmount => 'Eigene Anzahl...';
@override String get howManyEpisodes => 'Wie viele Episoden?';
}
// Path: playlists
@@ -1916,6 +1921,11 @@ extension on TranslationsDe {
'downloads.resumeAll' => 'Alle fortsetzen',
'downloads.deleteAll' => 'Alle löschen',
'downloads.selectVersion' => 'Version auswählen',
'downloads.allEpisodes' => 'Alle Episoden',
'downloads.unwatchedOnly' => 'Nur ungesehene',
'downloads.nextNUnwatched' => ({required Object count}) => 'Nächste ${count} ungesehene',
'downloads.customAmount' => 'Eigene Anzahl...',
'downloads.howManyEpisodes' => 'Wie viele Episoden?',
'playlists.title' => 'Wiedergabelisten',
'playlists.noPlaylists' => 'Keine Wiedergabelisten gefunden',
'playlists.create' => 'Wiedergabeliste erstellen',
+20
View File
@@ -2254,6 +2254,21 @@ class TranslationsDownloadsEn {
/// en: 'Select Version'
String get selectVersion => 'Select Version';
/// en: 'All episodes'
String get allEpisodes => 'All episodes';
/// en: 'Unwatched only'
String get unwatchedOnly => 'Unwatched only';
/// en: 'Next ${count} unwatched'
String nextNUnwatched({required Object count}) => 'Next ${count} unwatched';
/// en: 'Custom amount...'
String get customAmount => 'Custom amount...';
/// en: 'How many episodes?'
String get howManyEpisodes => 'How many episodes?';
}
// Path: shaders
@@ -3720,6 +3735,11 @@ extension on Translations {
'downloads.resumeAll' => 'Resume all',
'downloads.deleteAll' => 'Delete all',
'downloads.selectVersion' => 'Select Version',
'downloads.allEpisodes' => 'All episodes',
'downloads.unwatchedOnly' => 'Unwatched only',
'downloads.nextNUnwatched' => ({required Object count}) => 'Next ${count} unwatched',
'downloads.customAmount' => 'Custom amount...',
'downloads.howManyEpisodes' => 'How many episodes?',
'shaders.title' => 'Shaders',
'shaders.noShaderDescription' => 'No video enhancement',
'shaders.nvscalerDescription' => 'NVIDIA image scaling for sharper video',
+10
View File
@@ -994,6 +994,11 @@ class _TranslationsDownloadsEs implements TranslationsDownloadsEn {
@override String get resumeAll => 'Reanudar todo';
@override String get deleteAll => 'Eliminar todo';
@override String get selectVersion => 'Seleccionar versión';
@override String get allEpisodes => 'Todos los episodios';
@override String get unwatchedOnly => 'Solo no vistos';
@override String nextNUnwatched({required Object count}) => 'Próximos ${count} no vistos';
@override String get customAmount => 'Cantidad personalizada...';
@override String get howManyEpisodes => '¿Cuántos episodios?';
}
// Path: shaders
@@ -2013,6 +2018,11 @@ extension on TranslationsEs {
'downloads.resumeAll' => 'Reanudar todo',
'downloads.deleteAll' => 'Eliminar todo',
'downloads.selectVersion' => 'Seleccionar versión',
'downloads.allEpisodes' => 'Todos los episodios',
'downloads.unwatchedOnly' => 'Solo no vistos',
'downloads.nextNUnwatched' => ({required Object count}) => 'Próximos ${count} no vistos',
'downloads.customAmount' => 'Cantidad personalizada...',
'downloads.howManyEpisodes' => '¿Cuántos episodios?',
'shaders.title' => 'Shaders',
'shaders.noShaderDescription' => 'Sin mejora de video',
'shaders.nvscalerDescription' => 'Escalado de imagen NVIDIA para un video más nítido',
+10
View File
@@ -994,6 +994,11 @@ class _TranslationsDownloadsFr implements TranslationsDownloadsEn {
@override String get resumeAll => 'Tout reprendre';
@override String get deleteAll => 'Tout supprimer';
@override String get selectVersion => 'Sélectionner la version';
@override String get allEpisodes => 'Tous les épisodes';
@override String get unwatchedOnly => 'Non vus uniquement';
@override String nextNUnwatched({required Object count}) => '${count} prochains non vus';
@override String get customAmount => 'Quantité personnalisée...';
@override String get howManyEpisodes => 'Combien d\'épisodes ?';
}
// Path: shaders
@@ -2013,6 +2018,11 @@ extension on TranslationsFr {
'downloads.resumeAll' => 'Tout reprendre',
'downloads.deleteAll' => 'Tout supprimer',
'downloads.selectVersion' => 'Sélectionner la version',
'downloads.allEpisodes' => 'Tous les épisodes',
'downloads.unwatchedOnly' => 'Non vus uniquement',
'downloads.nextNUnwatched' => ({required Object count}) => '${count} prochains non vus',
'downloads.customAmount' => 'Quantité personnalisée...',
'downloads.howManyEpisodes' => 'Combien d\'épisodes ?',
'shaders.title' => 'Shaders',
'shaders.noShaderDescription' => 'Aucune amélioration vidéo',
'shaders.nvscalerDescription' => 'Mise à l\'échelle NVIDIA pour une vidéo plus nette',
+10
View File
@@ -870,6 +870,11 @@ class _TranslationsDownloadsIt implements TranslationsDownloadsEn {
@override String get resumeAll => 'Riprendi tutto';
@override String get deleteAll => 'Elimina tutto';
@override String get selectVersion => 'Seleziona versione';
@override String get allEpisodes => 'Tutti gli episodi';
@override String get unwatchedOnly => 'Solo non visti';
@override String nextNUnwatched({required Object count}) => 'Prossimi ${count} non visti';
@override String get customAmount => 'Quantità personalizzata...';
@override String get howManyEpisodes => 'Quanti episodi?';
}
// Path: playlists
@@ -1916,6 +1921,11 @@ extension on TranslationsIt {
'downloads.resumeAll' => 'Riprendi tutto',
'downloads.deleteAll' => 'Elimina tutto',
'downloads.selectVersion' => 'Seleziona versione',
'downloads.allEpisodes' => 'Tutti gli episodi',
'downloads.unwatchedOnly' => 'Solo non visti',
'downloads.nextNUnwatched' => ({required Object count}) => 'Prossimi ${count} non visti',
'downloads.customAmount' => 'Quantità personalizzata...',
'downloads.howManyEpisodes' => 'Quanti episodi?',
'playlists.title' => 'Playlist',
'playlists.noPlaylists' => 'Nessuna playlist trovata',
'playlists.create' => 'Crea playlist',
+10
View File
@@ -994,6 +994,11 @@ class _TranslationsDownloadsJa implements TranslationsDownloadsEn {
@override String get resumeAll => 'すべて再開';
@override String get deleteAll => 'すべて削除';
@override String get selectVersion => 'バージョンを選択';
@override String get allEpisodes => 'すべてのエピソード';
@override String get unwatchedOnly => '未視聴のみ';
@override String nextNUnwatched({required Object count}) => '次の${count}件の未視聴';
@override String get customAmount => '数を指定...';
@override String get howManyEpisodes => '何エピソード?';
}
// Path: shaders
@@ -2013,6 +2018,11 @@ extension on TranslationsJa {
'downloads.resumeAll' => 'すべて再開',
'downloads.deleteAll' => 'すべて削除',
'downloads.selectVersion' => 'バージョンを選択',
'downloads.allEpisodes' => 'すべてのエピソード',
'downloads.unwatchedOnly' => '未視聴のみ',
'downloads.nextNUnwatched' => ({required Object count}) => '次の${count}件の未視聴',
'downloads.customAmount' => '数を指定...',
'downloads.howManyEpisodes' => '何エピソード?',
'shaders.title' => 'シェーダー',
'shaders.noShaderDescription' => '映像補正なし',
'shaders.nvscalerDescription' => 'よりシャープな映像のためのNVIDIA画像スケーリング',
+10
View File
@@ -994,6 +994,11 @@ class _TranslationsDownloadsKo implements TranslationsDownloadsEn {
@override String get resumeAll => '모두 재개';
@override String get deleteAll => '모두 삭제';
@override String get selectVersion => '버전 선택';
@override String get allEpisodes => '모든 에피소드';
@override String get unwatchedOnly => '시청하지 않은 것만';
@override String nextNUnwatched({required Object count}) => '다음 ${count}개 미시청';
@override String get customAmount => '직접 입력...';
@override String get howManyEpisodes => '몇 개의 에피소드?';
}
// Path: shaders
@@ -2013,6 +2018,11 @@ extension on TranslationsKo {
'downloads.resumeAll' => '모두 재개',
'downloads.deleteAll' => '모두 삭제',
'downloads.selectVersion' => '버전 선택',
'downloads.allEpisodes' => '모든 에피소드',
'downloads.unwatchedOnly' => '시청하지 않은 것만',
'downloads.nextNUnwatched' => ({required Object count}) => '다음 ${count}개 미시청',
'downloads.customAmount' => '직접 입력...',
'downloads.howManyEpisodes' => '몇 개의 에피소드?',
'shaders.title' => '셰이더',
'shaders.noShaderDescription' => '비디오 향상 없음',
'shaders.nvscalerDescription' => '더 선명한 비디오를 위한 NVIDIA 이미지 스케일링',
+10
View File
@@ -994,6 +994,11 @@ class _TranslationsDownloadsNb implements TranslationsDownloadsEn {
@override String get resumeAll => 'Gjenoppta alle';
@override String get deleteAll => 'Slett alle';
@override String get selectVersion => 'Velg versjon';
@override String get allEpisodes => 'Alle episoder';
@override String get unwatchedOnly => 'Kun usette';
@override String nextNUnwatched({required Object count}) => 'Neste ${count} usette';
@override String get customAmount => 'Egendefinert antall...';
@override String get howManyEpisodes => 'Hvor mange episoder?';
}
// Path: shaders
@@ -2013,6 +2018,11 @@ extension on TranslationsNb {
'downloads.resumeAll' => 'Gjenoppta alle',
'downloads.deleteAll' => 'Slett alle',
'downloads.selectVersion' => 'Velg versjon',
'downloads.allEpisodes' => 'Alle episoder',
'downloads.unwatchedOnly' => 'Kun usette',
'downloads.nextNUnwatched' => ({required Object count}) => 'Neste ${count} usette',
'downloads.customAmount' => 'Egendefinert antall...',
'downloads.howManyEpisodes' => 'Hvor mange episoder?',
'shaders.title' => 'Shadere',
'shaders.noShaderDescription' => 'Ingen videoforbedring',
'shaders.nvscalerDescription' => 'NVIDIA bildeskalering for skarpere video',
+10
View File
@@ -870,6 +870,11 @@ class _TranslationsDownloadsNl implements TranslationsDownloadsEn {
@override String get resumeAll => 'Alles hervatten';
@override String get deleteAll => 'Alles verwijderen';
@override String get selectVersion => 'Versie selecteren';
@override String get allEpisodes => 'Alle afleveringen';
@override String get unwatchedOnly => 'Alleen onbekeken';
@override String nextNUnwatched({required Object count}) => 'Volgende ${count} onbekeken';
@override String get customAmount => 'Aangepast aantal...';
@override String get howManyEpisodes => 'Hoeveel afleveringen?';
}
// Path: playlists
@@ -1916,6 +1921,11 @@ extension on TranslationsNl {
'downloads.resumeAll' => 'Alles hervatten',
'downloads.deleteAll' => 'Alles verwijderen',
'downloads.selectVersion' => 'Versie selecteren',
'downloads.allEpisodes' => 'Alle afleveringen',
'downloads.unwatchedOnly' => 'Alleen onbekeken',
'downloads.nextNUnwatched' => ({required Object count}) => 'Volgende ${count} onbekeken',
'downloads.customAmount' => 'Aangepast aantal...',
'downloads.howManyEpisodes' => 'Hoeveel afleveringen?',
'playlists.title' => 'Afspeellijsten',
'playlists.noPlaylists' => 'Geen afspeellijsten gevonden',
'playlists.create' => 'Afspeellijst maken',
+10
View File
@@ -994,6 +994,11 @@ class _TranslationsDownloadsPl implements TranslationsDownloadsEn {
@override String get resumeAll => 'Wznów wszystko';
@override String get deleteAll => 'Usuń wszystko';
@override String get selectVersion => 'Wybierz wersję';
@override String get allEpisodes => 'Wszystkie odcinki';
@override String get unwatchedOnly => 'Tylko nieobejrzane';
@override String nextNUnwatched({required Object count}) => 'Następne ${count} nieobejrzanych';
@override String get customAmount => 'Własna ilość...';
@override String get howManyEpisodes => 'Ile odcinków?';
}
// Path: shaders
@@ -2013,6 +2018,11 @@ extension on TranslationsPl {
'downloads.resumeAll' => 'Wznów wszystko',
'downloads.deleteAll' => 'Usuń wszystko',
'downloads.selectVersion' => 'Wybierz wersję',
'downloads.allEpisodes' => 'Wszystkie odcinki',
'downloads.unwatchedOnly' => 'Tylko nieobejrzane',
'downloads.nextNUnwatched' => ({required Object count}) => 'Następne ${count} nieobejrzanych',
'downloads.customAmount' => 'Własna ilość...',
'downloads.howManyEpisodes' => 'Ile odcinków?',
'shaders.title' => 'Shadery',
'shaders.noShaderDescription' => 'Bez ulepszenia wideo',
'shaders.nvscalerDescription' => 'Skalowanie obrazu NVIDIA dla ostrzejszego wideo',
+10
View File
@@ -994,6 +994,11 @@ class _TranslationsDownloadsPt implements TranslationsDownloadsEn {
@override String get resumeAll => 'Retomar todos';
@override String get deleteAll => 'Excluir todos';
@override String get selectVersion => 'Selecionar versão';
@override String get allEpisodes => 'Todos os episódios';
@override String get unwatchedOnly => 'Apenas não assistidos';
@override String nextNUnwatched({required Object count}) => 'Próximos ${count} não assistidos';
@override String get customAmount => 'Quantidade personalizada...';
@override String get howManyEpisodes => 'Quantos episódios?';
}
// Path: shaders
@@ -2013,6 +2018,11 @@ extension on TranslationsPt {
'downloads.resumeAll' => 'Retomar todos',
'downloads.deleteAll' => 'Excluir todos',
'downloads.selectVersion' => 'Selecionar versão',
'downloads.allEpisodes' => 'Todos os episódios',
'downloads.unwatchedOnly' => 'Apenas não assistidos',
'downloads.nextNUnwatched' => ({required Object count}) => 'Próximos ${count} não assistidos',
'downloads.customAmount' => 'Quantidade personalizada...',
'downloads.howManyEpisodes' => 'Quantos episódios?',
'shaders.title' => 'Shaders',
'shaders.noShaderDescription' => 'Sem aprimoramento de vídeo',
'shaders.nvscalerDescription' => 'Escalonamento de imagem NVIDIA para vídeo mais nítido',
+10
View File
@@ -994,6 +994,11 @@ class _TranslationsDownloadsRu implements TranslationsDownloadsEn {
@override String get resumeAll => 'Возобновить все';
@override String get deleteAll => 'Удалить все';
@override String get selectVersion => 'Выбрать версию';
@override String get allEpisodes => 'Все эпизоды';
@override String get unwatchedOnly => 'Только непросмотренные';
@override String nextNUnwatched({required Object count}) => 'Следующие ${count} непросмотренных';
@override String get customAmount => 'Указать количество...';
@override String get howManyEpisodes => 'Сколько эпизодов?';
}
// Path: shaders
@@ -2013,6 +2018,11 @@ extension on TranslationsRu {
'downloads.resumeAll' => 'Возобновить все',
'downloads.deleteAll' => 'Удалить все',
'downloads.selectVersion' => 'Выбрать версию',
'downloads.allEpisodes' => 'Все эпизоды',
'downloads.unwatchedOnly' => 'Только непросмотренные',
'downloads.nextNUnwatched' => ({required Object count}) => 'Следующие ${count} непросмотренных',
'downloads.customAmount' => 'Указать количество...',
'downloads.howManyEpisodes' => 'Сколько эпизодов?',
'shaders.title' => 'Шейдеры',
'shaders.noShaderDescription' => 'Без улучшения видео',
'shaders.nvscalerDescription' => 'Масштабирование NVIDIA для более чёткого видео',
+10
View File
@@ -870,6 +870,11 @@ class _TranslationsDownloadsSv implements TranslationsDownloadsEn {
@override String get resumeAll => 'Återuppta alla';
@override String get deleteAll => 'Ta bort alla';
@override String get selectVersion => 'Välj version';
@override String get allEpisodes => 'Alla avsnitt';
@override String get unwatchedOnly => 'Endast osedda';
@override String nextNUnwatched({required Object count}) => 'Nästa ${count} osedda';
@override String get customAmount => 'Ange antal...';
@override String get howManyEpisodes => 'Hur många avsnitt?';
}
// Path: playlists
@@ -1916,6 +1921,11 @@ extension on TranslationsSv {
'downloads.resumeAll' => 'Återuppta alla',
'downloads.deleteAll' => 'Ta bort alla',
'downloads.selectVersion' => 'Välj version',
'downloads.allEpisodes' => 'Alla avsnitt',
'downloads.unwatchedOnly' => 'Endast osedda',
'downloads.nextNUnwatched' => ({required Object count}) => 'Nästa ${count} osedda',
'downloads.customAmount' => 'Ange antal...',
'downloads.howManyEpisodes' => 'Hur många avsnitt?',
'playlists.title' => 'Spellistor',
'playlists.noPlaylists' => 'Inga spellistor hittades',
'playlists.create' => 'Skapa spellista',
+10
View File
@@ -870,6 +870,11 @@ class _TranslationsDownloadsZh implements TranslationsDownloadsEn {
@override String get resumeAll => '全部继续';
@override String get deleteAll => '全部删除';
@override String get selectVersion => '选择版本';
@override String get allEpisodes => '所有剧集';
@override String get unwatchedOnly => '仅未观看';
@override String nextNUnwatched({required Object count}) => '接下来 ${count} 集未观看';
@override String get customAmount => '自定义数量...';
@override String get howManyEpisodes => '下载几集?';
}
// Path: playlists
@@ -1916,6 +1921,11 @@ extension on TranslationsZh {
'downloads.resumeAll' => '全部继续',
'downloads.deleteAll' => '全部删除',
'downloads.selectVersion' => '选择版本',
'downloads.allEpisodes' => '所有剧集',
'downloads.unwatchedOnly' => '仅未观看',
'downloads.nextNUnwatched' => ({required Object count}) => '接下来 ${count} 集未观看',
'downloads.customAmount' => '自定义数量...',
'downloads.howManyEpisodes' => '下载几集?',
'playlists.title' => '播放列表',
'playlists.noPlaylists' => '未找到播放列表',
'playlists.create' => '创建播放列表',
+6 -1
View File
@@ -630,7 +630,12 @@
"pauseAll": "Pausa alla",
"resumeAll": "Återuppta alla",
"deleteAll": "Ta bort alla",
"selectVersion": "Välj version"
"selectVersion": "Välj version",
"allEpisodes": "Alla avsnitt",
"unwatchedOnly": "Endast osedda",
"nextNUnwatched": "Nästa ${count} osedda",
"customAmount": "Ange antal...",
"howManyEpisodes": "Hur många avsnitt?"
},
"playlists": {
"title": "Spellistor",
+6 -1
View File
@@ -630,7 +630,12 @@
"pauseAll": "全部暂停",
"resumeAll": "全部继续",
"deleteAll": "全部删除",
"selectVersion": "选择版本"
"selectVersion": "选择版本",
"allEpisodes": "所有剧集",
"unwatchedOnly": "仅未观看",
"nextNUnwatched": "接下来 ${count} 集未观看",
"customAmount": "自定义数量...",
"howManyEpisodes": "下载几集?"
},
"playlists": {
"title": "播放列表",
+20 -5
View File
@@ -15,6 +15,10 @@ import '../services/plex_client.dart';
import '../utils/app_logger.dart';
import '../utils/global_key_utils.dart';
/// Filter mode for batch downloads (shows/seasons).
/// Use [all] to download everything, or [unwatched] with an optional maxCount.
enum DownloadFilter { all, unwatched }
/// Holds Plex thumb path reference for downloaded artwork.
/// The actual file path is computed from the hash of serverId + thumb path.
class DownloadedArtwork {
@@ -599,6 +603,8 @@ class DownloadProvider extends ChangeNotifier {
PlexMetadata metadata,
PlexClient client, {
DownloadVersionConfig? versionConfig,
DownloadFilter filter = DownloadFilter.all,
int? maxCount,
}) async {
final globalKey = metadata.globalKey;
final config = versionConfig ?? DownloadVersionConfig();
@@ -620,10 +626,10 @@ class DownloadProvider extends ChangeNotifier {
return queued ? 1 : 0;
} else if (mt == PlexMediaType.show) {
_metadata[globalKey] = metadata;
return await _queueShowDownload(metadata, client, versionConfig: config);
return await _queueShowDownload(metadata, client, versionConfig: config, filter: filter, maxCount: maxCount);
} else if (mt == PlexMediaType.season) {
_metadata[globalKey] = metadata;
return await _queueSeasonDownload(metadata, client, versionConfig: config);
return await _queueSeasonDownload(metadata, client, versionConfig: config, filter: filter, maxCount: maxCount);
} else {
throw Exception('Cannot download ${metadata.type}');
}
@@ -750,16 +756,22 @@ class DownloadProvider extends ChangeNotifier {
}
/// Queue all episodes from a TV show for download
Future<int> _queueShowDownload(PlexMetadata show, PlexClient client, {DownloadVersionConfig? versionConfig}) async {
Future<int> _queueShowDownload(PlexMetadata show, PlexClient client,
{DownloadVersionConfig? versionConfig, DownloadFilter filter = DownloadFilter.all, int? maxCount}) async {
int count = 0;
final seasons = await client.getChildren(show.ratingKey);
await _storeLeafCount(show.globalKey, show);
int? remaining = maxCount;
for (final season in seasons) {
if (season.type == 'season') {
if (remaining != null && remaining <= 0) break;
final seasonWithServer = _ensureServerId(season, show.serverId);
count += await _queueSeasonDownload(seasonWithServer, client, versionConfig: versionConfig);
final queued = await _queueSeasonDownload(seasonWithServer, client,
versionConfig: versionConfig, filter: filter, maxCount: remaining);
count += queued;
if (remaining != null) remaining -= queued;
}
}
@@ -768,7 +780,7 @@ class DownloadProvider extends ChangeNotifier {
/// Queue all episodes from a season for download
Future<int> _queueSeasonDownload(PlexMetadata season, PlexClient client,
{DownloadVersionConfig? versionConfig}) async {
{DownloadVersionConfig? versionConfig, DownloadFilter filter = DownloadFilter.all, int? maxCount}) async {
int count = 0;
final episodes = await client.getChildren(season.ratingKey);
@@ -776,6 +788,9 @@ class DownloadProvider extends ChangeNotifier {
for (final episode in episodes) {
if (episode.type == 'episode') {
if (filter == DownloadFilter.unwatched && episode.isWatched && !episode.hasActiveProgress) continue;
if (maxCount != null && count >= maxCount) break;
final episodeWithServer = _ensureServerId(episode, season.serverId);
final queued = await _queueSingleDownload(episodeWithServer, client, versionConfig: versionConfig);
if (queued) count++;
+13 -10
View File
@@ -30,6 +30,7 @@ import '../utils/rating_utils.dart';
import '../models/download_models.dart';
import '../services/download_storage_service.dart';
import '../utils/download_version_utils.dart';
import '../utils/download_utils.dart';
import '../providers/playback_state_provider.dart';
import '../providers/settings_provider.dart';
import '../utils/grid_size_calculator.dart';
@@ -777,17 +778,19 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
final client = _getClientForMetadata(context);
if (client == null) return;
final versionConfig = await _resolveDownloadVersion(context, metadata, client);
if (versionConfig == null || !context.mounted) return;
try {
final count = await downloadProvider.queueDownload(metadata, client, versionConfig: versionConfig);
if (context.mounted) {
final message = count > 1
? t.downloads.episodesQueued(count: count)
: t.downloads.downloadQueued;
showSuccessSnackBar(context, message);
}
final count = await showDownloadOptionsAndQueue(
context,
metadata: metadata,
client: client,
downloadProvider: downloadProvider,
);
if (count == null || !context.mounted) return;
final message = count > 1
? t.downloads.episodesQueued(count: count)
: t.downloads.downloadQueued;
showSuccessSnackBar(context, message);
} on CellularDownloadBlockedException {
if (context.mounted) {
showErrorSnackBar(context, t.settings.cellularDownloadBlocked);
+9 -4
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../focus/focusable_button.dart';
import '../focus/input_mode_tracker.dart';
import '../i18n/strings.g.dart';
import '../widgets/app_icon.dart';
import '../widgets/focusable_list_tile.dart';
@@ -281,17 +282,19 @@ Future<T?> showOptionPickerDialog<T>(
required String title,
required List<({IconData icon, String label, T value})> options,
}) {
final focusFirstItem = InputModeTracker.isKeyboardMode(context);
return showDialog<T>(
context: context,
builder: (context) => _OptionPickerDialog<T>(title: title, options: options),
builder: (context) => _OptionPickerDialog<T>(title: title, options: options, focusFirstItem: focusFirstItem),
);
}
class _OptionPickerDialog<T> extends StatefulWidget {
final String title;
final List<({IconData icon, String label, T value})> options;
final bool focusFirstItem;
const _OptionPickerDialog({required this.title, required this.options});
const _OptionPickerDialog({required this.title, required this.options, this.focusFirstItem = false});
@override
State<_OptionPickerDialog<T>> createState() => _OptionPickerDialogState<T>();
@@ -304,7 +307,9 @@ class _OptionPickerDialogState<T> extends State<_OptionPickerDialog<T>> {
void initState() {
super.initState();
_initialFocusNode = FocusNode(debugLabel: 'OptionPickerInitialFocus');
FocusUtils.requestFocusAfterBuild(this, _initialFocusNode);
if (widget.focusFirstItem) {
FocusUtils.requestFocusAfterBuild(this, _initialFocusNode);
}
}
@override
@@ -321,7 +326,7 @@ class _OptionPickerDialogState<T> extends State<_OptionPickerDialog<T>> {
children: List.generate(widget.options.length, (index) {
final option = widget.options[index];
return FocusableListTile(
focusNode: index == 0 ? _initialFocusNode : null,
focusNode: index == 0 && widget.focusFirstItem ? _initialFocusNode : null,
leading: AppIcon(option.icon, fill: 1, size: 24),
title: Text(option.label, style: Theme.of(context).textTheme.bodyLarge),
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
+143
View File
@@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../focus/focusable_button.dart';
import '../focus/input_mode_tracker.dart';
import '../i18n/strings.g.dart';
import '../models/plex_metadata.dart';
import '../providers/download_provider.dart';
import '../services/plex_client.dart';
import 'dialogs.dart';
import 'download_version_utils.dart';
/// Dialog option for the download picker. Typed to avoid stringly-typed values.
enum _DownloadChoice { all, unwatched, next5, next10, custom }
/// Shows download options dialog for shows/seasons, then queues the download.
/// For movies/episodes, queues directly without a dialog.
/// Returns the number of items queued, or null if cancelled.
Future<int?> showDownloadOptionsAndQueue(
BuildContext context, {
required PlexMetadata metadata,
required PlexClient client,
required DownloadProvider downloadProvider,
}) async {
final mt = metadata.mediaType;
var filter = DownloadFilter.all;
int? maxCount;
if (mt == PlexMediaType.show || mt == PlexMediaType.season) {
final selected = await showOptionPickerDialog<_DownloadChoice>(
context,
title: t.downloads.downloadNow,
options: [
(icon: Symbols.download_rounded, label: t.downloads.allEpisodes, value: _DownloadChoice.all),
(icon: Symbols.visibility_off_rounded, label: t.downloads.unwatchedOnly, value: _DownloadChoice.unwatched),
(icon: Symbols.filter_5_rounded, label: t.downloads.nextNUnwatched(count: 5), value: _DownloadChoice.next5),
(icon: Symbols.filter_9_plus_rounded, label: t.downloads.nextNUnwatched(count: 10), value: _DownloadChoice.next10),
(icon: Symbols.tune_rounded, label: t.downloads.customAmount, value: _DownloadChoice.custom),
],
);
if (selected == null || !context.mounted) return null;
switch (selected) {
case _DownloadChoice.all:
break;
case _DownloadChoice.unwatched:
filter = DownloadFilter.unwatched;
case _DownloadChoice.next5:
filter = DownloadFilter.unwatched;
maxCount = 5;
case _DownloadChoice.next10:
filter = DownloadFilter.unwatched;
maxCount = 10;
case _DownloadChoice.custom:
final count = await _showEpisodeCountDialog(context);
if (count == null || !context.mounted) return null;
filter = DownloadFilter.unwatched;
maxCount = count;
}
}
if (!context.mounted) return null;
final versionConfig = await resolveDownloadVersion(context, metadata, client);
if (versionConfig == null || !context.mounted) return null;
return await downloadProvider.queueDownload(
metadata,
client,
versionConfig: versionConfig,
filter: filter,
maxCount: maxCount,
);
}
Future<int?> _showEpisodeCountDialog(BuildContext context) {
final autoFocus = InputModeTracker.isKeyboardMode(context);
return showDialog<int>(
context: context,
builder: (context) => _EpisodeCountDialog(autoFocus: autoFocus),
);
}
class _EpisodeCountDialog extends StatefulWidget {
final bool autoFocus;
const _EpisodeCountDialog({this.autoFocus = false});
@override
State<_EpisodeCountDialog> createState() => _EpisodeCountDialogState();
}
class _EpisodeCountDialogState extends State<_EpisodeCountDialog> {
late final TextEditingController _controller;
final _submitFocusNode = FocusNode();
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
void dispose() {
_controller.dispose();
_submitFocusNode.dispose();
super.dispose();
}
void _submit() {
final count = int.tryParse(_controller.text);
if (count != null && count > 0) {
Navigator.pop(context, count);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(t.downloads.howManyEpisodes),
content: TextField(
controller: _controller,
autofocus: widget.autoFocus,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
textInputAction: TextInputAction.done,
onSubmitted: (_) => _submitFocusNode.requestFocus(),
),
actions: [
FocusableButton(
onPressed: () => Navigator.pop(context),
child: TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)),
),
FocusableButton(
focusNode: _submitFocusNode,
onPressed: _submit,
child: TextButton(onPressed: _submit, child: Text(t.common.ok)),
),
],
);
}
}
+10 -8
View File
@@ -8,6 +8,7 @@ import '../services/play_queue_launcher.dart';
import '../models/plex_metadata.dart';
import '../models/plex_playlist.dart';
import '../utils/download_version_utils.dart';
import '../utils/download_utils.dart';
import '../utils/content_utils.dart';
import '../providers/download_provider.dart';
import '../providers/multi_server_provider.dart';
@@ -1115,15 +1116,16 @@ class MediaContextMenuState extends State<MediaContextMenu> {
final client = _getClientForItem();
try {
final versionConfig = await resolveDownloadVersion(context, metadata, client);
if (versionConfig == null) return;
if (!context.mounted) return;
final count = await showDownloadOptionsAndQueue(
context,
metadata: metadata,
client: client,
downloadProvider: downloadProvider,
);
if (count == null || !context.mounted) return;
final count = await downloadProvider.queueDownload(metadata, client, versionConfig: versionConfig);
if (context.mounted) {
final message = count > 1 ? t.downloads.episodesQueued(count: count) : t.downloads.downloadQueued;
showSuccessSnackBar(context, message);
}
final message = count > 1 ? t.downloads.episodesQueued(count: count) : t.downloads.downloadQueued;
showSuccessSnackBar(context, message);
} on CellularDownloadBlockedException {
if (context.mounted) {
showErrorSnackBar(context, t.settings.cellularDownloadBlocked);