feat: download all playlist items

close #241
This commit is contained in:
edde746
2026-04-08 12:37:03 +02:00
parent 8d40ba3011
commit d0934295b0
35 changed files with 184 additions and 17 deletions
+2 -1
View File
@@ -738,7 +738,8 @@
"unwatchedOnly": "Kun usete", "unwatchedOnly": "Kun usete",
"nextNUnwatched": "Næste ${count} usete", "nextNUnwatched": "Næste ${count} usete",
"customAmount": "Angiv antal...", "customAmount": "Angiv antal...",
"howManyEpisodes": "Hvor mange episoder?" "howManyEpisodes": "Hvor mange episoder?",
"itemsQueued": "${count} elementer sat i kø til download"
}, },
"shaders": { "shaders": {
"title": "Shadere", "title": "Shadere",
+2 -1
View File
@@ -635,7 +635,8 @@
"unwatchedOnly": "Nur ungesehene", "unwatchedOnly": "Nur ungesehene",
"nextNUnwatched": "Nächste ${count} ungesehene", "nextNUnwatched": "Nächste ${count} ungesehene",
"customAmount": "Eigene Anzahl...", "customAmount": "Eigene Anzahl...",
"howManyEpisodes": "Wie viele Episoden?" "howManyEpisodes": "Wie viele Episoden?",
"itemsQueued": "${count} Elemente zum Download eingereiht"
}, },
"playlists": { "playlists": {
"title": "Wiedergabelisten", "title": "Wiedergabelisten",
+2 -1
View File
@@ -738,7 +738,8 @@
"unwatchedOnly": "Unwatched only", "unwatchedOnly": "Unwatched only",
"nextNUnwatched": "Next ${count} unwatched", "nextNUnwatched": "Next ${count} unwatched",
"customAmount": "Custom amount...", "customAmount": "Custom amount...",
"howManyEpisodes": "How many episodes?" "howManyEpisodes": "How many episodes?",
"itemsQueued": "${count} items queued for download"
}, },
"shaders": { "shaders": {
"title": "Shaders", "title": "Shaders",
+2 -1
View File
@@ -738,7 +738,8 @@
"unwatchedOnly": "Solo no vistos", "unwatchedOnly": "Solo no vistos",
"nextNUnwatched": "Próximos ${count} no vistos", "nextNUnwatched": "Próximos ${count} no vistos",
"customAmount": "Cantidad personalizada...", "customAmount": "Cantidad personalizada...",
"howManyEpisodes": "¿Cuántos episodios?" "howManyEpisodes": "¿Cuántos episodios?",
"itemsQueued": "${count} elementos en cola de descarga"
}, },
"shaders": { "shaders": {
"title": "Shaders", "title": "Shaders",
+2 -1
View File
@@ -738,7 +738,8 @@
"unwatchedOnly": "Non vus uniquement", "unwatchedOnly": "Non vus uniquement",
"nextNUnwatched": "${count} prochains non vus", "nextNUnwatched": "${count} prochains non vus",
"customAmount": "Quantité personnalisée...", "customAmount": "Quantité personnalisée...",
"howManyEpisodes": "Combien d'épisodes ?" "howManyEpisodes": "Combien d'épisodes ?",
"itemsQueued": "${count} éléments mis en file d'attente"
}, },
"shaders": { "shaders": {
"title": "Shaders", "title": "Shaders",
+2 -1
View File
@@ -635,7 +635,8 @@
"unwatchedOnly": "Solo non visti", "unwatchedOnly": "Solo non visti",
"nextNUnwatched": "Prossimi ${count} non visti", "nextNUnwatched": "Prossimi ${count} non visti",
"customAmount": "Quantità personalizzata...", "customAmount": "Quantità personalizzata...",
"howManyEpisodes": "Quanti episodi?" "howManyEpisodes": "Quanti episodi?",
"itemsQueued": "${count} elementi in coda per il download"
}, },
"playlists": { "playlists": {
"title": "Playlist", "title": "Playlist",
+2 -1
View File
@@ -738,7 +738,8 @@
"unwatchedOnly": "未視聴のみ", "unwatchedOnly": "未視聴のみ",
"nextNUnwatched": "次の${count}件の未視聴", "nextNUnwatched": "次の${count}件の未視聴",
"customAmount": "数を指定...", "customAmount": "数を指定...",
"howManyEpisodes": "何エピソード?" "howManyEpisodes": "何エピソード?",
"itemsQueued": "${count}件をダウンロードキューに追加"
}, },
"shaders": { "shaders": {
"title": "シェーダー", "title": "シェーダー",
+2 -1
View File
@@ -738,7 +738,8 @@
"unwatchedOnly": "시청하지 않은 것만", "unwatchedOnly": "시청하지 않은 것만",
"nextNUnwatched": "다음 ${count}개 미시청", "nextNUnwatched": "다음 ${count}개 미시청",
"customAmount": "직접 입력...", "customAmount": "직접 입력...",
"howManyEpisodes": "몇 개의 에피소드?" "howManyEpisodes": "몇 개의 에피소드?",
"itemsQueued": "${count}개 항목이 다운로드 대기열에 추가됨"
}, },
"shaders": { "shaders": {
"title": "셰이더", "title": "셰이더",
+2 -1
View File
@@ -738,7 +738,8 @@
"unwatchedOnly": "Kun usette", "unwatchedOnly": "Kun usette",
"nextNUnwatched": "Neste ${count} usette", "nextNUnwatched": "Neste ${count} usette",
"customAmount": "Egendefinert antall...", "customAmount": "Egendefinert antall...",
"howManyEpisodes": "Hvor mange episoder?" "howManyEpisodes": "Hvor mange episoder?",
"itemsQueued": "${count} elementer i nedlastingskø"
}, },
"shaders": { "shaders": {
"title": "Shadere", "title": "Shadere",
+2 -1
View File
@@ -635,7 +635,8 @@
"unwatchedOnly": "Alleen onbekeken", "unwatchedOnly": "Alleen onbekeken",
"nextNUnwatched": "Volgende ${count} onbekeken", "nextNUnwatched": "Volgende ${count} onbekeken",
"customAmount": "Aangepast aantal...", "customAmount": "Aangepast aantal...",
"howManyEpisodes": "Hoeveel afleveringen?" "howManyEpisodes": "Hoeveel afleveringen?",
"itemsQueued": "${count} items in downloadwachtrij"
}, },
"playlists": { "playlists": {
"title": "Afspeellijsten", "title": "Afspeellijsten",
+2 -1
View File
@@ -738,7 +738,8 @@
"unwatchedOnly": "Tylko nieobejrzane", "unwatchedOnly": "Tylko nieobejrzane",
"nextNUnwatched": "Następne ${count} nieobejrzanych", "nextNUnwatched": "Następne ${count} nieobejrzanych",
"customAmount": "Własna ilość...", "customAmount": "Własna ilość...",
"howManyEpisodes": "Ile odcinków?" "howManyEpisodes": "Ile odcinków?",
"itemsQueued": "${count} elementów dodanych do kolejki pobierania"
}, },
"shaders": { "shaders": {
"title": "Shadery", "title": "Shadery",
+2 -1
View File
@@ -738,7 +738,8 @@
"unwatchedOnly": "Apenas não assistidos", "unwatchedOnly": "Apenas não assistidos",
"nextNUnwatched": "Próximos ${count} não assistidos", "nextNUnwatched": "Próximos ${count} não assistidos",
"customAmount": "Quantidade personalizada...", "customAmount": "Quantidade personalizada...",
"howManyEpisodes": "Quantos episódios?" "howManyEpisodes": "Quantos episódios?",
"itemsQueued": "${count} itens na fila de download"
}, },
"shaders": { "shaders": {
"title": "Shaders", "title": "Shaders",
+2 -1
View File
@@ -738,7 +738,8 @@
"unwatchedOnly": "Только непросмотренные", "unwatchedOnly": "Только непросмотренные",
"nextNUnwatched": "Следующие ${count} непросмотренных", "nextNUnwatched": "Следующие ${count} непросмотренных",
"customAmount": "Указать количество...", "customAmount": "Указать количество...",
"howManyEpisodes": "Сколько эпизодов?" "howManyEpisodes": "Сколько эпизодов?",
"itemsQueued": "${count} элементов добавлено в очередь загрузки"
}, },
"shaders": { "shaders": {
"title": "Шейдеры", "title": "Шейдеры",
+2 -2
View File
@@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 15 /// Locales: 15
/// Strings: 12825 (855 per locale) /// Strings: 12840 (856 per locale)
/// ///
/// Built on 2026-04-07 at 18:27 UTC /// Built on 2026-04-08 at 10:00 UTC
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint, unused_import // ignore_for_file: type=lint, unused_import
+2
View File
@@ -999,6 +999,7 @@ class _TranslationsDownloadsDa implements TranslationsDownloadsEn {
@override String nextNUnwatched({required Object count}) => 'Næste ${count} usete'; @override String nextNUnwatched({required Object count}) => 'Næste ${count} usete';
@override String get customAmount => 'Angiv antal...'; @override String get customAmount => 'Angiv antal...';
@override String get howManyEpisodes => 'Hvor mange episoder?'; @override String get howManyEpisodes => 'Hvor mange episoder?';
@override String itemsQueued({required Object count}) => '${count} elementer sat i kø til download';
} }
// Path: shaders // Path: shaders
@@ -2023,6 +2024,7 @@ extension on TranslationsDa {
'downloads.nextNUnwatched' => ({required Object count}) => 'Næste ${count} usete', 'downloads.nextNUnwatched' => ({required Object count}) => 'Næste ${count} usete',
'downloads.customAmount' => 'Angiv antal...', 'downloads.customAmount' => 'Angiv antal...',
'downloads.howManyEpisodes' => 'Hvor mange episoder?', 'downloads.howManyEpisodes' => 'Hvor mange episoder?',
'downloads.itemsQueued' => ({required Object count}) => '${count} elementer sat i kø til download',
'shaders.title' => 'Shadere', 'shaders.title' => 'Shadere',
'shaders.noShaderDescription' => 'Ingen videoforbedring', 'shaders.noShaderDescription' => 'Ingen videoforbedring',
'shaders.nvscalerDescription' => 'NVIDIA-billedskalering for skarpere video', 'shaders.nvscalerDescription' => 'NVIDIA-billedskalering for skarpere video',
+2
View File
@@ -875,6 +875,7 @@ class _TranslationsDownloadsDe implements TranslationsDownloadsEn {
@override String nextNUnwatched({required Object count}) => 'Nächste ${count} ungesehene'; @override String nextNUnwatched({required Object count}) => 'Nächste ${count} ungesehene';
@override String get customAmount => 'Eigene Anzahl...'; @override String get customAmount => 'Eigene Anzahl...';
@override String get howManyEpisodes => 'Wie viele Episoden?'; @override String get howManyEpisodes => 'Wie viele Episoden?';
@override String itemsQueued({required Object count}) => '${count} Elemente zum Download eingereiht';
} }
// Path: playlists // Path: playlists
@@ -1926,6 +1927,7 @@ extension on TranslationsDe {
'downloads.nextNUnwatched' => ({required Object count}) => 'Nächste ${count} ungesehene', 'downloads.nextNUnwatched' => ({required Object count}) => 'Nächste ${count} ungesehene',
'downloads.customAmount' => 'Eigene Anzahl...', 'downloads.customAmount' => 'Eigene Anzahl...',
'downloads.howManyEpisodes' => 'Wie viele Episoden?', 'downloads.howManyEpisodes' => 'Wie viele Episoden?',
'downloads.itemsQueued' => ({required Object count}) => '${count} Elemente zum Download eingereiht',
'playlists.title' => 'Wiedergabelisten', 'playlists.title' => 'Wiedergabelisten',
'playlists.noPlaylists' => 'Keine Wiedergabelisten gefunden', 'playlists.noPlaylists' => 'Keine Wiedergabelisten gefunden',
'playlists.create' => 'Wiedergabeliste erstellen', 'playlists.create' => 'Wiedergabeliste erstellen',
+4
View File
@@ -2269,6 +2269,9 @@ class TranslationsDownloadsEn {
/// en: 'How many episodes?' /// en: 'How many episodes?'
String get howManyEpisodes => 'How many episodes?'; String get howManyEpisodes => 'How many episodes?';
/// en: '${count} items queued for download'
String itemsQueued({required Object count}) => '${count} items queued for download';
} }
// Path: shaders // Path: shaders
@@ -3740,6 +3743,7 @@ extension on Translations {
'downloads.nextNUnwatched' => ({required Object count}) => 'Next ${count} unwatched', 'downloads.nextNUnwatched' => ({required Object count}) => 'Next ${count} unwatched',
'downloads.customAmount' => 'Custom amount...', 'downloads.customAmount' => 'Custom amount...',
'downloads.howManyEpisodes' => 'How many episodes?', 'downloads.howManyEpisodes' => 'How many episodes?',
'downloads.itemsQueued' => ({required Object count}) => '${count} items queued for download',
'shaders.title' => 'Shaders', 'shaders.title' => 'Shaders',
'shaders.noShaderDescription' => 'No video enhancement', 'shaders.noShaderDescription' => 'No video enhancement',
'shaders.nvscalerDescription' => 'NVIDIA image scaling for sharper video', 'shaders.nvscalerDescription' => 'NVIDIA image scaling for sharper video',
+2
View File
@@ -999,6 +999,7 @@ class _TranslationsDownloadsEs implements TranslationsDownloadsEn {
@override String nextNUnwatched({required Object count}) => 'Próximos ${count} no vistos'; @override String nextNUnwatched({required Object count}) => 'Próximos ${count} no vistos';
@override String get customAmount => 'Cantidad personalizada...'; @override String get customAmount => 'Cantidad personalizada...';
@override String get howManyEpisodes => '¿Cuántos episodios?'; @override String get howManyEpisodes => '¿Cuántos episodios?';
@override String itemsQueued({required Object count}) => '${count} elementos en cola de descarga';
} }
// Path: shaders // Path: shaders
@@ -2023,6 +2024,7 @@ extension on TranslationsEs {
'downloads.nextNUnwatched' => ({required Object count}) => 'Próximos ${count} no vistos', 'downloads.nextNUnwatched' => ({required Object count}) => 'Próximos ${count} no vistos',
'downloads.customAmount' => 'Cantidad personalizada...', 'downloads.customAmount' => 'Cantidad personalizada...',
'downloads.howManyEpisodes' => '¿Cuántos episodios?', 'downloads.howManyEpisodes' => '¿Cuántos episodios?',
'downloads.itemsQueued' => ({required Object count}) => '${count} elementos en cola de descarga',
'shaders.title' => 'Shaders', 'shaders.title' => 'Shaders',
'shaders.noShaderDescription' => 'Sin mejora de video', 'shaders.noShaderDescription' => 'Sin mejora de video',
'shaders.nvscalerDescription' => 'Escalado de imagen NVIDIA para un video más nítido', 'shaders.nvscalerDescription' => 'Escalado de imagen NVIDIA para un video más nítido',
+2
View File
@@ -999,6 +999,7 @@ class _TranslationsDownloadsFr implements TranslationsDownloadsEn {
@override String nextNUnwatched({required Object count}) => '${count} prochains non vus'; @override String nextNUnwatched({required Object count}) => '${count} prochains non vus';
@override String get customAmount => 'Quantité personnalisée...'; @override String get customAmount => 'Quantité personnalisée...';
@override String get howManyEpisodes => 'Combien d\'épisodes ?'; @override String get howManyEpisodes => 'Combien d\'épisodes ?';
@override String itemsQueued({required Object count}) => '${count} éléments mis en file d\'attente';
} }
// Path: shaders // Path: shaders
@@ -2023,6 +2024,7 @@ extension on TranslationsFr {
'downloads.nextNUnwatched' => ({required Object count}) => '${count} prochains non vus', 'downloads.nextNUnwatched' => ({required Object count}) => '${count} prochains non vus',
'downloads.customAmount' => 'Quantité personnalisée...', 'downloads.customAmount' => 'Quantité personnalisée...',
'downloads.howManyEpisodes' => 'Combien d\'épisodes ?', 'downloads.howManyEpisodes' => 'Combien d\'épisodes ?',
'downloads.itemsQueued' => ({required Object count}) => '${count} éléments mis en file d\'attente',
'shaders.title' => 'Shaders', 'shaders.title' => 'Shaders',
'shaders.noShaderDescription' => 'Aucune amélioration vidéo', 'shaders.noShaderDescription' => 'Aucune amélioration vidéo',
'shaders.nvscalerDescription' => 'Mise à l\'échelle NVIDIA pour une vidéo plus nette', 'shaders.nvscalerDescription' => 'Mise à l\'échelle NVIDIA pour une vidéo plus nette',
+2
View File
@@ -875,6 +875,7 @@ class _TranslationsDownloadsIt implements TranslationsDownloadsEn {
@override String nextNUnwatched({required Object count}) => 'Prossimi ${count} non visti'; @override String nextNUnwatched({required Object count}) => 'Prossimi ${count} non visti';
@override String get customAmount => 'Quantità personalizzata...'; @override String get customAmount => 'Quantità personalizzata...';
@override String get howManyEpisodes => 'Quanti episodi?'; @override String get howManyEpisodes => 'Quanti episodi?';
@override String itemsQueued({required Object count}) => '${count} elementi in coda per il download';
} }
// Path: playlists // Path: playlists
@@ -1926,6 +1927,7 @@ extension on TranslationsIt {
'downloads.nextNUnwatched' => ({required Object count}) => 'Prossimi ${count} non visti', 'downloads.nextNUnwatched' => ({required Object count}) => 'Prossimi ${count} non visti',
'downloads.customAmount' => 'Quantità personalizzata...', 'downloads.customAmount' => 'Quantità personalizzata...',
'downloads.howManyEpisodes' => 'Quanti episodi?', 'downloads.howManyEpisodes' => 'Quanti episodi?',
'downloads.itemsQueued' => ({required Object count}) => '${count} elementi in coda per il download',
'playlists.title' => 'Playlist', 'playlists.title' => 'Playlist',
'playlists.noPlaylists' => 'Nessuna playlist trovata', 'playlists.noPlaylists' => 'Nessuna playlist trovata',
'playlists.create' => 'Crea playlist', 'playlists.create' => 'Crea playlist',
+2
View File
@@ -999,6 +999,7 @@ class _TranslationsDownloadsJa implements TranslationsDownloadsEn {
@override String nextNUnwatched({required Object count}) => '次の${count}件の未視聴'; @override String nextNUnwatched({required Object count}) => '次の${count}件の未視聴';
@override String get customAmount => '数を指定...'; @override String get customAmount => '数を指定...';
@override String get howManyEpisodes => '何エピソード?'; @override String get howManyEpisodes => '何エピソード?';
@override String itemsQueued({required Object count}) => '${count}件をダウンロードキューに追加';
} }
// Path: shaders // Path: shaders
@@ -2023,6 +2024,7 @@ extension on TranslationsJa {
'downloads.nextNUnwatched' => ({required Object count}) => '次の${count}件の未視聴', 'downloads.nextNUnwatched' => ({required Object count}) => '次の${count}件の未視聴',
'downloads.customAmount' => '数を指定...', 'downloads.customAmount' => '数を指定...',
'downloads.howManyEpisodes' => '何エピソード?', 'downloads.howManyEpisodes' => '何エピソード?',
'downloads.itemsQueued' => ({required Object count}) => '${count}件をダウンロードキューに追加',
'shaders.title' => 'シェーダー', 'shaders.title' => 'シェーダー',
'shaders.noShaderDescription' => '映像補正なし', 'shaders.noShaderDescription' => '映像補正なし',
'shaders.nvscalerDescription' => 'よりシャープな映像のためのNVIDIA画像スケーリング', 'shaders.nvscalerDescription' => 'よりシャープな映像のためのNVIDIA画像スケーリング',
+2
View File
@@ -999,6 +999,7 @@ class _TranslationsDownloadsKo implements TranslationsDownloadsEn {
@override String nextNUnwatched({required Object count}) => '다음 ${count}개 미시청'; @override String nextNUnwatched({required Object count}) => '다음 ${count}개 미시청';
@override String get customAmount => '직접 입력...'; @override String get customAmount => '직접 입력...';
@override String get howManyEpisodes => '몇 개의 에피소드?'; @override String get howManyEpisodes => '몇 개의 에피소드?';
@override String itemsQueued({required Object count}) => '${count}개 항목이 다운로드 대기열에 추가됨';
} }
// Path: shaders // Path: shaders
@@ -2023,6 +2024,7 @@ extension on TranslationsKo {
'downloads.nextNUnwatched' => ({required Object count}) => '다음 ${count}개 미시청', 'downloads.nextNUnwatched' => ({required Object count}) => '다음 ${count}개 미시청',
'downloads.customAmount' => '직접 입력...', 'downloads.customAmount' => '직접 입력...',
'downloads.howManyEpisodes' => '몇 개의 에피소드?', 'downloads.howManyEpisodes' => '몇 개의 에피소드?',
'downloads.itemsQueued' => ({required Object count}) => '${count}개 항목이 다운로드 대기열에 추가됨',
'shaders.title' => '셰이더', 'shaders.title' => '셰이더',
'shaders.noShaderDescription' => '비디오 향상 없음', 'shaders.noShaderDescription' => '비디오 향상 없음',
'shaders.nvscalerDescription' => '더 선명한 비디오를 위한 NVIDIA 이미지 스케일링', 'shaders.nvscalerDescription' => '더 선명한 비디오를 위한 NVIDIA 이미지 스케일링',
+2
View File
@@ -999,6 +999,7 @@ class _TranslationsDownloadsNb implements TranslationsDownloadsEn {
@override String nextNUnwatched({required Object count}) => 'Neste ${count} usette'; @override String nextNUnwatched({required Object count}) => 'Neste ${count} usette';
@override String get customAmount => 'Egendefinert antall...'; @override String get customAmount => 'Egendefinert antall...';
@override String get howManyEpisodes => 'Hvor mange episoder?'; @override String get howManyEpisodes => 'Hvor mange episoder?';
@override String itemsQueued({required Object count}) => '${count} elementer i nedlastingskø';
} }
// Path: shaders // Path: shaders
@@ -2023,6 +2024,7 @@ extension on TranslationsNb {
'downloads.nextNUnwatched' => ({required Object count}) => 'Neste ${count} usette', 'downloads.nextNUnwatched' => ({required Object count}) => 'Neste ${count} usette',
'downloads.customAmount' => 'Egendefinert antall...', 'downloads.customAmount' => 'Egendefinert antall...',
'downloads.howManyEpisodes' => 'Hvor mange episoder?', 'downloads.howManyEpisodes' => 'Hvor mange episoder?',
'downloads.itemsQueued' => ({required Object count}) => '${count} elementer i nedlastingskø',
'shaders.title' => 'Shadere', 'shaders.title' => 'Shadere',
'shaders.noShaderDescription' => 'Ingen videoforbedring', 'shaders.noShaderDescription' => 'Ingen videoforbedring',
'shaders.nvscalerDescription' => 'NVIDIA bildeskalering for skarpere video', 'shaders.nvscalerDescription' => 'NVIDIA bildeskalering for skarpere video',
+2
View File
@@ -875,6 +875,7 @@ class _TranslationsDownloadsNl implements TranslationsDownloadsEn {
@override String nextNUnwatched({required Object count}) => 'Volgende ${count} onbekeken'; @override String nextNUnwatched({required Object count}) => 'Volgende ${count} onbekeken';
@override String get customAmount => 'Aangepast aantal...'; @override String get customAmount => 'Aangepast aantal...';
@override String get howManyEpisodes => 'Hoeveel afleveringen?'; @override String get howManyEpisodes => 'Hoeveel afleveringen?';
@override String itemsQueued({required Object count}) => '${count} items in downloadwachtrij';
} }
// Path: playlists // Path: playlists
@@ -1926,6 +1927,7 @@ extension on TranslationsNl {
'downloads.nextNUnwatched' => ({required Object count}) => 'Volgende ${count} onbekeken', 'downloads.nextNUnwatched' => ({required Object count}) => 'Volgende ${count} onbekeken',
'downloads.customAmount' => 'Aangepast aantal...', 'downloads.customAmount' => 'Aangepast aantal...',
'downloads.howManyEpisodes' => 'Hoeveel afleveringen?', 'downloads.howManyEpisodes' => 'Hoeveel afleveringen?',
'downloads.itemsQueued' => ({required Object count}) => '${count} items in downloadwachtrij',
'playlists.title' => 'Afspeellijsten', 'playlists.title' => 'Afspeellijsten',
'playlists.noPlaylists' => 'Geen afspeellijsten gevonden', 'playlists.noPlaylists' => 'Geen afspeellijsten gevonden',
'playlists.create' => 'Afspeellijst maken', 'playlists.create' => 'Afspeellijst maken',
+2
View File
@@ -999,6 +999,7 @@ class _TranslationsDownloadsPl implements TranslationsDownloadsEn {
@override String nextNUnwatched({required Object count}) => 'Następne ${count} nieobejrzanych'; @override String nextNUnwatched({required Object count}) => 'Następne ${count} nieobejrzanych';
@override String get customAmount => 'Własna ilość...'; @override String get customAmount => 'Własna ilość...';
@override String get howManyEpisodes => 'Ile odcinków?'; @override String get howManyEpisodes => 'Ile odcinków?';
@override String itemsQueued({required Object count}) => '${count} elementów dodanych do kolejki pobierania';
} }
// Path: shaders // Path: shaders
@@ -2023,6 +2024,7 @@ extension on TranslationsPl {
'downloads.nextNUnwatched' => ({required Object count}) => 'Następne ${count} nieobejrzanych', 'downloads.nextNUnwatched' => ({required Object count}) => 'Następne ${count} nieobejrzanych',
'downloads.customAmount' => 'Własna ilość...', 'downloads.customAmount' => 'Własna ilość...',
'downloads.howManyEpisodes' => 'Ile odcinków?', 'downloads.howManyEpisodes' => 'Ile odcinków?',
'downloads.itemsQueued' => ({required Object count}) => '${count} elementów dodanych do kolejki pobierania',
'shaders.title' => 'Shadery', 'shaders.title' => 'Shadery',
'shaders.noShaderDescription' => 'Bez ulepszenia wideo', 'shaders.noShaderDescription' => 'Bez ulepszenia wideo',
'shaders.nvscalerDescription' => 'Skalowanie obrazu NVIDIA dla ostrzejszego wideo', 'shaders.nvscalerDescription' => 'Skalowanie obrazu NVIDIA dla ostrzejszego wideo',
+2
View File
@@ -999,6 +999,7 @@ class _TranslationsDownloadsPt implements TranslationsDownloadsEn {
@override String nextNUnwatched({required Object count}) => 'Próximos ${count} não assistidos'; @override String nextNUnwatched({required Object count}) => 'Próximos ${count} não assistidos';
@override String get customAmount => 'Quantidade personalizada...'; @override String get customAmount => 'Quantidade personalizada...';
@override String get howManyEpisodes => 'Quantos episódios?'; @override String get howManyEpisodes => 'Quantos episódios?';
@override String itemsQueued({required Object count}) => '${count} itens na fila de download';
} }
// Path: shaders // Path: shaders
@@ -2023,6 +2024,7 @@ extension on TranslationsPt {
'downloads.nextNUnwatched' => ({required Object count}) => 'Próximos ${count} não assistidos', 'downloads.nextNUnwatched' => ({required Object count}) => 'Próximos ${count} não assistidos',
'downloads.customAmount' => 'Quantidade personalizada...', 'downloads.customAmount' => 'Quantidade personalizada...',
'downloads.howManyEpisodes' => 'Quantos episódios?', 'downloads.howManyEpisodes' => 'Quantos episódios?',
'downloads.itemsQueued' => ({required Object count}) => '${count} itens na fila de download',
'shaders.title' => 'Shaders', 'shaders.title' => 'Shaders',
'shaders.noShaderDescription' => 'Sem aprimoramento de vídeo', 'shaders.noShaderDescription' => 'Sem aprimoramento de vídeo',
'shaders.nvscalerDescription' => 'Escalonamento de imagem NVIDIA para vídeo mais nítido', 'shaders.nvscalerDescription' => 'Escalonamento de imagem NVIDIA para vídeo mais nítido',
+2
View File
@@ -999,6 +999,7 @@ class _TranslationsDownloadsRu implements TranslationsDownloadsEn {
@override String nextNUnwatched({required Object count}) => 'Следующие ${count} непросмотренных'; @override String nextNUnwatched({required Object count}) => 'Следующие ${count} непросмотренных';
@override String get customAmount => 'Указать количество...'; @override String get customAmount => 'Указать количество...';
@override String get howManyEpisodes => 'Сколько эпизодов?'; @override String get howManyEpisodes => 'Сколько эпизодов?';
@override String itemsQueued({required Object count}) => '${count} элементов добавлено в очередь загрузки';
} }
// Path: shaders // Path: shaders
@@ -2023,6 +2024,7 @@ extension on TranslationsRu {
'downloads.nextNUnwatched' => ({required Object count}) => 'Следующие ${count} непросмотренных', 'downloads.nextNUnwatched' => ({required Object count}) => 'Следующие ${count} непросмотренных',
'downloads.customAmount' => 'Указать количество...', 'downloads.customAmount' => 'Указать количество...',
'downloads.howManyEpisodes' => 'Сколько эпизодов?', 'downloads.howManyEpisodes' => 'Сколько эпизодов?',
'downloads.itemsQueued' => ({required Object count}) => '${count} элементов добавлено в очередь загрузки',
'shaders.title' => 'Шейдеры', 'shaders.title' => 'Шейдеры',
'shaders.noShaderDescription' => 'Без улучшения видео', 'shaders.noShaderDescription' => 'Без улучшения видео',
'shaders.nvscalerDescription' => 'Масштабирование NVIDIA для более чёткого видео', 'shaders.nvscalerDescription' => 'Масштабирование NVIDIA для более чёткого видео',
+2
View File
@@ -875,6 +875,7 @@ class _TranslationsDownloadsSv implements TranslationsDownloadsEn {
@override String nextNUnwatched({required Object count}) => 'Nästa ${count} osedda'; @override String nextNUnwatched({required Object count}) => 'Nästa ${count} osedda';
@override String get customAmount => 'Ange antal...'; @override String get customAmount => 'Ange antal...';
@override String get howManyEpisodes => 'Hur många avsnitt?'; @override String get howManyEpisodes => 'Hur många avsnitt?';
@override String itemsQueued({required Object count}) => '${count} objekt köade för nedladdning';
} }
// Path: playlists // Path: playlists
@@ -1926,6 +1927,7 @@ extension on TranslationsSv {
'downloads.nextNUnwatched' => ({required Object count}) => 'Nästa ${count} osedda', 'downloads.nextNUnwatched' => ({required Object count}) => 'Nästa ${count} osedda',
'downloads.customAmount' => 'Ange antal...', 'downloads.customAmount' => 'Ange antal...',
'downloads.howManyEpisodes' => 'Hur många avsnitt?', 'downloads.howManyEpisodes' => 'Hur många avsnitt?',
'downloads.itemsQueued' => ({required Object count}) => '${count} objekt köade för nedladdning',
'playlists.title' => 'Spellistor', 'playlists.title' => 'Spellistor',
'playlists.noPlaylists' => 'Inga spellistor hittades', 'playlists.noPlaylists' => 'Inga spellistor hittades',
'playlists.create' => 'Skapa spellista', 'playlists.create' => 'Skapa spellista',
+2
View File
@@ -875,6 +875,7 @@ class _TranslationsDownloadsZh implements TranslationsDownloadsEn {
@override String nextNUnwatched({required Object count}) => '接下来 ${count} 集未观看'; @override String nextNUnwatched({required Object count}) => '接下来 ${count} 集未观看';
@override String get customAmount => '自定义数量...'; @override String get customAmount => '自定义数量...';
@override String get howManyEpisodes => '下载几集?'; @override String get howManyEpisodes => '下载几集?';
@override String itemsQueued({required Object count}) => '${count} 个项目已加入下载队列';
} }
// Path: playlists // Path: playlists
@@ -1926,6 +1927,7 @@ extension on TranslationsZh {
'downloads.nextNUnwatched' => ({required Object count}) => '接下来 ${count} 集未观看', 'downloads.nextNUnwatched' => ({required Object count}) => '接下来 ${count} 集未观看',
'downloads.customAmount' => '自定义数量...', 'downloads.customAmount' => '自定义数量...',
'downloads.howManyEpisodes' => '下载几集?', 'downloads.howManyEpisodes' => '下载几集?',
'downloads.itemsQueued' => ({required Object count}) => '${count} 个项目已加入下载队列',
'playlists.title' => '播放列表', 'playlists.title' => '播放列表',
'playlists.noPlaylists' => '未找到播放列表', 'playlists.noPlaylists' => '未找到播放列表',
'playlists.create' => '创建播放列表', 'playlists.create' => '创建播放列表',
+2 -1
View File
@@ -635,7 +635,8 @@
"unwatchedOnly": "Endast osedda", "unwatchedOnly": "Endast osedda",
"nextNUnwatched": "Nästa ${count} osedda", "nextNUnwatched": "Nästa ${count} osedda",
"customAmount": "Ange antal...", "customAmount": "Ange antal...",
"howManyEpisodes": "Hur många avsnitt?" "howManyEpisodes": "Hur många avsnitt?",
"itemsQueued": "${count} objekt köade för nedladdning"
}, },
"playlists": { "playlists": {
"title": "Spellistor", "title": "Spellistor",
+2 -1
View File
@@ -635,7 +635,8 @@
"unwatchedOnly": "仅未观看", "unwatchedOnly": "仅未观看",
"nextNUnwatched": "接下来 ${count} 集未观看", "nextNUnwatched": "接下来 ${count} 集未观看",
"customAmount": "自定义数量...", "customAmount": "自定义数量...",
"howManyEpisodes": "下载几集?" "howManyEpisodes": "下载几集?",
"itemsQueued": "${count} 个项目已加入下载队列"
}, },
"playlists": { "playlists": {
"title": "播放列表", "title": "播放列表",
+23
View File
@@ -639,6 +639,29 @@ class DownloadProvider extends ChangeNotifier {
} }
} }
/// Queue all video items from a playlist for download.
/// Returns the number of items queued.
Future<int> queuePlaylistDownload(
List<PlexMetadata> items,
PlexClient client, {
DownloadFilter filter = DownloadFilter.all,
}) async {
if (await DownloadManagerService.shouldBlockDownloadOnCellular()) {
throw CellularDownloadBlockedException();
}
int count = 0;
for (final item in items) {
final mt = item.mediaType;
if (mt != PlexMediaType.movie && mt != PlexMediaType.episode) continue;
if (filter == DownloadFilter.unwatched && item.isWatched && !item.hasActiveProgress) continue;
final queued = await _queueSingleDownload(item, client);
if (queued) count++;
}
return count;
}
/// Queue a single movie or episode for download. /// Queue a single movie or episode for download.
/// Returns true if the item was actually queued, false if skipped. /// Returns true if the item was actually queued, false if skipped.
Future<bool> _queueSingleDownload( Future<bool> _queueSingleDownload(
@@ -13,9 +13,12 @@ import '../../widgets/desktop_app_bar.dart';
import '../../focus/dpad_navigator.dart'; import '../../focus/dpad_navigator.dart';
import '../../focus/input_mode_tracker.dart'; import '../../focus/input_mode_tracker.dart';
import '../../focus/key_event_utils.dart'; import '../../focus/key_event_utils.dart';
import 'package:provider/provider.dart';
import 'playlist_item_card.dart'; import 'playlist_item_card.dart';
import '../../i18n/strings.g.dart'; import '../../i18n/strings.g.dart';
import '../../providers/download_provider.dart';
import '../../utils/dialogs.dart'; import '../../utils/dialogs.dart';
import '../../utils/download_utils.dart';
import '../../utils/snackbar_helper.dart'; import '../../utils/snackbar_helper.dart';
import '../base_media_list_detail_screen.dart'; import '../base_media_list_detail_screen.dart';
import '../focusable_detail_screen_mixin.dart'; import '../focusable_detail_screen_mixin.dart';
@@ -58,6 +61,8 @@ class _PlaylistDetailScreenState extends BaseMediaListDetailScreen<PlaylistDetai
FocusableAction(icon: Symbols.play_arrow_rounded, tooltip: t.common.play, onPressed: playItems), FocusableAction(icon: Symbols.play_arrow_rounded, tooltip: t.common.play, onPressed: playItems),
FocusableAction(icon: Symbols.shuffle_rounded, tooltip: t.common.shuffle, onPressed: shufflePlayItems), FocusableAction(icon: Symbols.shuffle_rounded, tooltip: t.common.shuffle, onPressed: shufflePlayItems),
], ],
if (items.isNotEmpty && widget.playlist.playlistType == 'video')
FocusableAction(icon: Symbols.download_rounded, tooltip: t.downloads.downloadNow, onPressed: _downloadPlaylist),
if (!widget.playlist.smart) if (!widget.playlist.smart)
FocusableAction(icon: Symbols.delete_rounded, tooltip: t.playlists.delete, onPressed: _deletePlaylist, iconColor: Colors.red), FocusableAction(icon: Symbols.delete_rounded, tooltip: t.playlists.delete, onPressed: _deletePlaylist, iconColor: Colors.red),
]; ];
@@ -139,6 +144,31 @@ class _PlaylistDetailScreenState extends BaseMediaListDetailScreen<PlaylistDetai
return context.getClientForServer(widget.playlist.serverId!); return context.getClientForServer(widget.playlist.serverId!);
} }
Future<void> _downloadPlaylist() async {
final downloadProvider = Provider.of<DownloadProvider>(context, listen: false);
try {
final count = await showPlaylistDownloadOptionsAndQueue(
context,
items: items,
client: client,
downloadProvider: downloadProvider,
);
if (count == null || !mounted) return;
final message = count > 1 ? t.downloads.itemsQueued(count: count) : t.downloads.downloadQueued;
showSuccessSnackBar(context, message);
} on CellularDownloadBlockedException {
if (mounted) {
showErrorSnackBar(context, t.settings.cellularDownloadBlocked);
}
} catch (e) {
if (mounted) {
showErrorSnackBar(context, t.messages.errorLoading(error: e.toString()));
}
}
}
Future<void> _deletePlaylist() async { Future<void> _deletePlaylist() async {
final confirmed = await showDeleteConfirmation( final confirmed = await showDeleteConfirmation(
context, context,
+26
View File
@@ -75,6 +75,32 @@ Future<int?> showDownloadOptionsAndQueue(
); );
} }
/// Shows download options dialog for playlists, then queues the download.
/// Returns the number of items queued, or null if cancelled.
Future<int?> showPlaylistDownloadOptionsAndQueue(
BuildContext context, {
required List<PlexMetadata> items,
required PlexClient client,
required DownloadProvider downloadProvider,
}) async {
final selected = await showOptionPickerDialog<DownloadFilter>(
context,
title: t.downloads.downloadNow,
options: [
(icon: Symbols.download_rounded, label: t.downloads.allEpisodes, value: DownloadFilter.all),
(icon: Symbols.visibility_off_rounded, label: t.downloads.unwatchedOnly, value: DownloadFilter.unwatched),
],
);
if (selected == null || !context.mounted) return null;
return await downloadProvider.queuePlaylistDownload(
items,
client,
filter: selected,
);
}
Future<int?> _showEpisodeCountDialog(BuildContext context) { Future<int?> _showEpisodeCountDialog(BuildContext context) {
final autoFocus = InputModeTracker.isKeyboardMode(context); final autoFocus = InputModeTracker.isKeyboardMode(context);
return showDialog<int>( return showDialog<int>(
+41
View File
@@ -158,6 +158,11 @@ class MediaContextMenuState extends State<MediaContextMenu> {
// Shuffle // Shuffle
menuActions.add(_MenuAction(value: 'shuffle', icon: Symbols.shuffle_rounded, label: t.mediaMenu.shufflePlay)); menuActions.add(_MenuAction(value: 'shuffle', icon: Symbols.shuffle_rounded, label: t.mediaMenu.shufflePlay));
// Download (video playlists only)
if (isPlaylist && (widget.item as PlexPlaylist).playlistType == 'video') {
menuActions.add(_MenuAction(value: 'download_playlist', icon: Symbols.download_rounded, label: t.downloads.downloadNow));
}
// Delete // Delete
menuActions.add(_MenuAction(value: 'delete', icon: Symbols.delete_rounded, label: t.common.delete)); menuActions.add(_MenuAction(value: 'delete', icon: Symbols.delete_rounded, label: t.common.delete));
@@ -534,6 +539,10 @@ class MediaContextMenuState extends State<MediaContextMenu> {
await _handlePlayExternal(context); await _handlePlayExternal(context);
break; break;
case 'download_playlist':
await _handleDownloadPlaylist(context);
break;
case 'download': case 'download':
await _handleDownload(context); await _handleDownload(context);
break; break;
@@ -1109,6 +1118,38 @@ class MediaContextMenuState extends State<MediaContextMenu> {
await ExternalPlayerService.launch(context: context, metadata: metadata, client: client); await ExternalPlayerService.launch(context: context, metadata: metadata, client: client);
} }
/// Handle download playlist action
Future<void> _handleDownloadPlaylist(BuildContext context) async {
final playlist = widget.item as PlexPlaylist;
final downloadProvider = Provider.of<DownloadProvider>(context, listen: false);
final client = _getClientForItem();
try {
final items = await client.getPlaylist(playlist.ratingKey);
if (!context.mounted) return;
final count = await showPlaylistDownloadOptionsAndQueue(
context,
items: items,
client: client,
downloadProvider: downloadProvider,
);
if (count == null || !context.mounted) return;
final message = count > 1 ? t.downloads.itemsQueued(count: count) : t.downloads.downloadQueued;
showSuccessSnackBar(context, message);
} on CellularDownloadBlockedException {
if (context.mounted) {
showErrorSnackBar(context, t.settings.cellularDownloadBlocked);
}
} catch (e) {
appLogger.e('Failed to queue playlist download', error: e);
if (context.mounted) {
showErrorSnackBar(context, t.messages.errorLoading(error: e.toString()));
}
}
}
/// Handle download action /// Handle download action
Future<void> _handleDownload(BuildContext context) async { Future<void> _handleDownload(BuildContext context) async {
final downloadProvider = Provider.of<DownloadProvider>(context, listen: false); final downloadProvider = Provider.of<DownloadProvider>(context, listen: false);