feat(livetv): let user pick and remember the recording target library

close #1513
This commit is contained in:
edde746
2026-07-10 06:41:15 +02:00
parent 2c13438d71
commit 632d2ec16b
37 changed files with 257 additions and 49 deletions
+1
View File
@@ -879,6 +879,7 @@
"recordEpisode": "Запиши епизод",
"recordSeries": "Запиши сериал",
"recordOptions": "Опции за запис",
"saveTo": "Запази в",
"recordings": "Записи",
"scheduledRecordings": "Планирани",
"recordingRules": "Правила за запис",
+1
View File
@@ -879,6 +879,7 @@
"recordEpisode": "Optag episode",
"recordSeries": "Optag serie",
"recordOptions": "Optageindstillinger",
"saveTo": "Gem i",
"recordings": "Optagelser",
"scheduledRecordings": "Planlagt",
"recordingRules": "Optagelsesregler",
+1
View File
@@ -879,6 +879,7 @@
"recordEpisode": "Episode aufnehmen",
"recordSeries": "Serie aufnehmen",
"recordOptions": "Aufnahmeoptionen",
"saveTo": "Speichern in",
"recordings": "Aufnahmen",
"scheduledRecordings": "Geplant",
"recordingRules": "Aufnahmeregeln",
+1
View File
@@ -891,6 +891,7 @@
"recordEpisode": "Record Episode",
"recordSeries": "Record Series",
"recordOptions": "Record Options",
"saveTo": "Save to",
"recordings": "Recordings",
"scheduledRecordings": "Scheduled",
"recordingRules": "Recording Rules",
+1
View File
@@ -879,6 +879,7 @@
"recordEpisode": "Grabar episodio",
"recordSeries": "Grabar serie",
"recordOptions": "Opciones de grabación",
"saveTo": "Guardar en",
"recordings": "Grabaciones",
"scheduledRecordings": "Programadas",
"recordingRules": "Reglas de grabación",
+1
View File
@@ -879,6 +879,7 @@
"recordEpisode": "Enregistrer l'épisode",
"recordSeries": "Enregistrer la série",
"recordOptions": "Options d'enregistrement",
"saveTo": "Enregistrer dans",
"recordings": "Enregistrements",
"scheduledRecordings": "Programmés",
"recordingRules": "Règles d'enregistrement",
+1
View File
@@ -879,6 +879,7 @@
"recordEpisode": "Registra episodio",
"recordSeries": "Registra serie",
"recordOptions": "Opzioni di registrazione",
"saveTo": "Salva in",
"recordings": "Registrazioni",
"scheduledRecordings": "Programmate",
"recordingRules": "Regole di registrazione",
+1
View File
@@ -879,6 +879,7 @@
"recordEpisode": "このエピソードを録画",
"recordSeries": "シリーズを録画",
"recordOptions": "録画オプション",
"saveTo": "保存先",
"recordings": "録画",
"scheduledRecordings": "予約",
"recordingRules": "録画ルール",
+1
View File
@@ -879,6 +879,7 @@
"recordEpisode": "에피소드 녹화",
"recordSeries": "시리즈 녹화",
"recordOptions": "녹화 옵션",
"saveTo": "저장 위치",
"recordings": "녹화",
"scheduledRecordings": "예약됨",
"recordingRules": "녹화 규칙",
+1
View File
@@ -879,6 +879,7 @@
"recordEpisode": "Ta opp episode",
"recordSeries": "Ta opp serie",
"recordOptions": "Opptaksvalg",
"saveTo": "Lagre i",
"recordings": "Opptak",
"scheduledRecordings": "Planlagt",
"recordingRules": "Opptaksregler",
+1
View File
@@ -879,6 +879,7 @@
"recordEpisode": "Aflevering opnemen",
"recordSeries": "Serie opnemen",
"recordOptions": "Opnameopties",
"saveTo": "Opslaan in",
"recordings": "Opnames",
"scheduledRecordings": "Gepland",
"recordingRules": "Opnameregels",
+1
View File
@@ -879,6 +879,7 @@
"recordEpisode": "Nagraj odcinek",
"recordSeries": "Nagraj serial",
"recordOptions": "Opcje nagrywania",
"saveTo": "Zapisz w",
"recordings": "Nagrania",
"scheduledRecordings": "Zaplanowane",
"recordingRules": "Reguły nagrywania",
+1
View File
@@ -879,6 +879,7 @@
"recordEpisode": "Gravar episódio",
"recordSeries": "Gravar série",
"recordOptions": "Opções de gravação",
"saveTo": "Salvar em",
"recordings": "Gravações",
"scheduledRecordings": "Agendadas",
"recordingRules": "Regras de gravação",
+1
View File
@@ -879,6 +879,7 @@
"recordEpisode": "Записать эпизод",
"recordSeries": "Записать сериал",
"recordOptions": "Параметры записи",
"saveTo": "Сохранить в",
"recordings": "Записи",
"scheduledRecordings": "Запланировано",
"recordingRules": "Правила записи",
+1 -1
View File
@@ -4,7 +4,7 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 16
/// Strings: 21360 (1335 per locale)
/// Strings: 21376 (1336 per locale)
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
+3 -1
View File
@@ -1106,6 +1106,7 @@ class _TranslationsLiveTvBg extends TranslationsLiveTvEn {
@override String get recordEpisode => 'Запиши епизод';
@override String get recordSeries => 'Запиши сериал';
@override String get recordOptions => 'Опции за запис';
@override String get saveTo => 'Запази в';
@override String get recordings => 'Записи';
@override String get scheduledRecordings => 'Планирани';
@override String get recordingRules => 'Правила за запис';
@@ -2804,6 +2805,7 @@ extension on TranslationsBg {
'liveTv.recordEpisode' => 'Запиши епизод',
'liveTv.recordSeries' => 'Запиши сериал',
'liveTv.recordOptions' => 'Опции за запис',
'liveTv.saveTo' => 'Запази в',
'liveTv.recordings' => 'Записи',
'liveTv.scheduledRecordings' => 'Планирани',
'liveTv.recordingRules' => 'Правила за запис',
@@ -3020,9 +3022,9 @@ extension on TranslationsBg {
'downloads.noSyncRules' => 'Няма правила за синхронизация',
'downloads.manageSyncRule' => 'Управление на синхронизацията',
'downloads.editEpisodeCount' => 'Брой епизоди',
'downloads.editSyncFilter' => 'Филтър за синхронизация',
_ => null,
} ?? switch (path) {
'downloads.editSyncFilter' => 'Филтър за синхронизация',
'downloads.syncAllItems' => 'Синхронизират се всички елементи',
'downloads.syncUnwatchedItems' => 'Синхронизират се негледаните елементи',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Сървър: ${server}${status}',
+3 -1
View File
@@ -1106,6 +1106,7 @@ class _TranslationsLiveTvDa extends TranslationsLiveTvEn {
@override String get recordEpisode => 'Optag episode';
@override String get recordSeries => 'Optag serie';
@override String get recordOptions => 'Optageindstillinger';
@override String get saveTo => 'Gem i';
@override String get recordings => 'Optagelser';
@override String get scheduledRecordings => 'Planlagt';
@override String get recordingRules => 'Optagelsesregler';
@@ -2804,6 +2805,7 @@ extension on TranslationsDa {
'liveTv.recordEpisode' => 'Optag episode',
'liveTv.recordSeries' => 'Optag serie',
'liveTv.recordOptions' => 'Optageindstillinger',
'liveTv.saveTo' => 'Gem i',
'liveTv.recordings' => 'Optagelser',
'liveTv.scheduledRecordings' => 'Planlagt',
'liveTv.recordingRules' => 'Optagelsesregler',
@@ -3020,9 +3022,9 @@ extension on TranslationsDa {
'downloads.noSyncRules' => 'Ingen synkroniseringsregler',
'downloads.manageSyncRule' => 'Administrer synkronisering',
'downloads.editEpisodeCount' => 'Antal episoder',
'downloads.editSyncFilter' => 'Synkroniseringsfilter',
_ => null,
} ?? switch (path) {
'downloads.editSyncFilter' => 'Synkroniseringsfilter',
'downloads.syncAllItems' => 'Synkroniserer alle elementer',
'downloads.syncUnwatchedItems' => 'Synkroniserer usete elementer',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}',
+3 -1
View File
@@ -1106,6 +1106,7 @@ class _TranslationsLiveTvDe extends TranslationsLiveTvEn {
@override String get recordEpisode => 'Episode aufnehmen';
@override String get recordSeries => 'Serie aufnehmen';
@override String get recordOptions => 'Aufnahmeoptionen';
@override String get saveTo => 'Speichern in';
@override String get recordings => 'Aufnahmen';
@override String get scheduledRecordings => 'Geplant';
@override String get recordingRules => 'Aufnahmeregeln';
@@ -2804,6 +2805,7 @@ extension on TranslationsDe {
'liveTv.recordEpisode' => 'Episode aufnehmen',
'liveTv.recordSeries' => 'Serie aufnehmen',
'liveTv.recordOptions' => 'Aufnahmeoptionen',
'liveTv.saveTo' => 'Speichern in',
'liveTv.recordings' => 'Aufnahmen',
'liveTv.scheduledRecordings' => 'Geplant',
'liveTv.recordingRules' => 'Aufnahmeregeln',
@@ -3020,9 +3022,9 @@ extension on TranslationsDe {
'downloads.noSyncRules' => 'Keine Sync-Regeln',
'downloads.manageSyncRule' => 'Synchronisierung verwalten',
'downloads.editEpisodeCount' => 'Episodenanzahl',
'downloads.editSyncFilter' => 'Synchronisierungsfilter',
_ => null,
} ?? switch (path) {
'downloads.editSyncFilter' => 'Synchronisierungsfilter',
'downloads.syncAllItems' => 'Alle Einträge synchronisieren',
'downloads.syncUnwatchedItems' => 'Ungesehene Einträge synchronisieren',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}',
+5 -1
View File
@@ -2614,6 +2614,9 @@ class TranslationsLiveTvEn {
/// en: 'Record Options'
String get recordOptions => 'Record Options';
/// en: 'Save to'
String get saveTo => 'Save to';
/// en: 'Recordings'
String get recordings => 'Recordings';
@@ -5533,6 +5536,7 @@ extension on Translations {
'liveTv.recordEpisode' => 'Record Episode',
'liveTv.recordSeries' => 'Record Series',
'liveTv.recordOptions' => 'Record Options',
'liveTv.saveTo' => 'Save to',
'liveTv.recordings' => 'Recordings',
'liveTv.scheduledRecordings' => 'Scheduled',
'liveTv.recordingRules' => 'Recording Rules',
@@ -5737,9 +5741,9 @@ extension on Translations {
'downloads.customAmount' => 'Custom amount...',
'downloads.includeSpecials' => 'Include Specials',
'downloads.howManyEpisodes' => 'How many episodes?',
'downloads.itemsQueued' => ({required Object count}) => '${count} items queued for download',
_ => null,
} ?? switch (path) {
'downloads.itemsQueued' => ({required Object count}) => '${count} items queued for download',
'downloads.keepSynced' => 'Keep synced',
'downloads.downloadOnce' => 'Download once',
'downloads.keepNUnwatched' => ({required Object count}) => 'Keep ${count} unwatched',
+3 -1
View File
@@ -1106,6 +1106,7 @@ class _TranslationsLiveTvEs extends TranslationsLiveTvEn {
@override String get recordEpisode => 'Grabar episodio';
@override String get recordSeries => 'Grabar serie';
@override String get recordOptions => 'Opciones de grabación';
@override String get saveTo => 'Guardar en';
@override String get recordings => 'Grabaciones';
@override String get scheduledRecordings => 'Programadas';
@override String get recordingRules => 'Reglas de grabación';
@@ -2804,6 +2805,7 @@ extension on TranslationsEs {
'liveTv.recordEpisode' => 'Grabar episodio',
'liveTv.recordSeries' => 'Grabar serie',
'liveTv.recordOptions' => 'Opciones de grabación',
'liveTv.saveTo' => 'Guardar en',
'liveTv.recordings' => 'Grabaciones',
'liveTv.scheduledRecordings' => 'Programadas',
'liveTv.recordingRules' => 'Reglas de grabación',
@@ -3020,9 +3022,9 @@ extension on TranslationsEs {
'downloads.noSyncRules' => 'Sin reglas de sincronización',
'downloads.manageSyncRule' => 'Gestionar sincronización',
'downloads.editEpisodeCount' => 'Número de episodios',
'downloads.editSyncFilter' => 'Filtro de sincronización',
_ => null,
} ?? switch (path) {
'downloads.editSyncFilter' => 'Filtro de sincronización',
'downloads.syncAllItems' => 'Sincronizando todos los elementos',
'downloads.syncUnwatchedItems' => 'Sincronizando elementos no vistos',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Servidor: ${server}${status}',
+3 -1
View File
@@ -1106,6 +1106,7 @@ class _TranslationsLiveTvFr extends TranslationsLiveTvEn {
@override String get recordEpisode => 'Enregistrer l\'épisode';
@override String get recordSeries => 'Enregistrer la série';
@override String get recordOptions => 'Options d\'enregistrement';
@override String get saveTo => 'Enregistrer dans';
@override String get recordings => 'Enregistrements';
@override String get scheduledRecordings => 'Programmés';
@override String get recordingRules => 'Règles d\'enregistrement';
@@ -2804,6 +2805,7 @@ extension on TranslationsFr {
'liveTv.recordEpisode' => 'Enregistrer l\'épisode',
'liveTv.recordSeries' => 'Enregistrer la série',
'liveTv.recordOptions' => 'Options d\'enregistrement',
'liveTv.saveTo' => 'Enregistrer dans',
'liveTv.recordings' => 'Enregistrements',
'liveTv.scheduledRecordings' => 'Programmés',
'liveTv.recordingRules' => 'Règles d\'enregistrement',
@@ -3020,9 +3022,9 @@ extension on TranslationsFr {
'downloads.noSyncRules' => 'Aucune règle de synchronisation',
'downloads.manageSyncRule' => 'Gérer la synchronisation',
'downloads.editEpisodeCount' => 'Nombre d’épisodes',
'downloads.editSyncFilter' => 'Filtre de synchronisation',
_ => null,
} ?? switch (path) {
'downloads.editSyncFilter' => 'Filtre de synchronisation',
'downloads.syncAllItems' => 'Synchronisation de tous les éléments',
'downloads.syncUnwatchedItems' => 'Synchronisation des éléments non vus',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Serveur : ${server}${status}',
+3 -1
View File
@@ -1106,6 +1106,7 @@ class _TranslationsLiveTvIt extends TranslationsLiveTvEn {
@override String get recordEpisode => 'Registra episodio';
@override String get recordSeries => 'Registra serie';
@override String get recordOptions => 'Opzioni di registrazione';
@override String get saveTo => 'Salva in';
@override String get recordings => 'Registrazioni';
@override String get scheduledRecordings => 'Programmate';
@override String get recordingRules => 'Regole di registrazione';
@@ -2804,6 +2805,7 @@ extension on TranslationsIt {
'liveTv.recordEpisode' => 'Registra episodio',
'liveTv.recordSeries' => 'Registra serie',
'liveTv.recordOptions' => 'Opzioni di registrazione',
'liveTv.saveTo' => 'Salva in',
'liveTv.recordings' => 'Registrazioni',
'liveTv.scheduledRecordings' => 'Programmate',
'liveTv.recordingRules' => 'Regole di registrazione',
@@ -3020,9 +3022,9 @@ extension on TranslationsIt {
'downloads.noSyncRules' => 'Nessuna regola di sincronizzazione',
'downloads.manageSyncRule' => 'Gestisci sincronizzazione',
'downloads.editEpisodeCount' => 'Numero di episodi',
'downloads.editSyncFilter' => 'Filtro di sincronizzazione',
_ => null,
} ?? switch (path) {
'downloads.editSyncFilter' => 'Filtro di sincronizzazione',
'downloads.syncAllItems' => 'Sincronizzazione di tutti gli elementi',
'downloads.syncUnwatchedItems' => 'Sincronizzazione degli elementi non visti',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}',
+3 -1
View File
@@ -1106,6 +1106,7 @@ class _TranslationsLiveTvJa extends TranslationsLiveTvEn {
@override String get recordEpisode => 'このエピソードを録画';
@override String get recordSeries => 'シリーズを録画';
@override String get recordOptions => '録画オプション';
@override String get saveTo => '保存先';
@override String get recordings => '録画';
@override String get scheduledRecordings => '予約';
@override String get recordingRules => '録画ルール';
@@ -2803,6 +2804,7 @@ extension on TranslationsJa {
'liveTv.recordEpisode' => 'このエピソードを録画',
'liveTv.recordSeries' => 'シリーズを録画',
'liveTv.recordOptions' => '録画オプション',
'liveTv.saveTo' => '保存先',
'liveTv.recordings' => '録画',
'liveTv.scheduledRecordings' => '予約',
'liveTv.recordingRules' => '録画ルール',
@@ -3019,9 +3021,9 @@ extension on TranslationsJa {
'downloads.noSyncRules' => '同期ルールなし',
'downloads.manageSyncRule' => '同期を管理',
'downloads.editEpisodeCount' => 'エピソード数',
'downloads.editSyncFilter' => '同期フィルター',
_ => null,
} ?? switch (path) {
'downloads.editSyncFilter' => '同期フィルター',
'downloads.syncAllItems' => 'すべてのアイテムを同期中',
'downloads.syncUnwatchedItems' => '未視聴のアイテムを同期中',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'サーバー: ${server}${status}',
+3 -1
View File
@@ -1106,6 +1106,7 @@ class _TranslationsLiveTvKo extends TranslationsLiveTvEn {
@override String get recordEpisode => '에피소드 녹화';
@override String get recordSeries => '시리즈 녹화';
@override String get recordOptions => '녹화 옵션';
@override String get saveTo => '저장 위치';
@override String get recordings => '녹화';
@override String get scheduledRecordings => '예약됨';
@override String get recordingRules => '녹화 규칙';
@@ -2803,6 +2804,7 @@ extension on TranslationsKo {
'liveTv.recordEpisode' => '에피소드 녹화',
'liveTv.recordSeries' => '시리즈 녹화',
'liveTv.recordOptions' => '녹화 옵션',
'liveTv.saveTo' => '저장 위치',
'liveTv.recordings' => '녹화',
'liveTv.scheduledRecordings' => '예약됨',
'liveTv.recordingRules' => '녹화 규칙',
@@ -3019,9 +3021,9 @@ extension on TranslationsKo {
'downloads.noSyncRules' => '동기화 규칙 없음',
'downloads.manageSyncRule' => '동기화 관리',
'downloads.editEpisodeCount' => '에피소드 수',
'downloads.editSyncFilter' => '동기화 필터',
_ => null,
} ?? switch (path) {
'downloads.editSyncFilter' => '동기화 필터',
'downloads.syncAllItems' => '모든 항목 동기화 중',
'downloads.syncUnwatchedItems' => '시청하지 않은 항목 동기화 중',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => '서버: ${server}${status}',
+3 -1
View File
@@ -1106,6 +1106,7 @@ class _TranslationsLiveTvNb extends TranslationsLiveTvEn {
@override String get recordEpisode => 'Ta opp episode';
@override String get recordSeries => 'Ta opp serie';
@override String get recordOptions => 'Opptaksvalg';
@override String get saveTo => 'Lagre i';
@override String get recordings => 'Opptak';
@override String get scheduledRecordings => 'Planlagt';
@override String get recordingRules => 'Opptaksregler';
@@ -2804,6 +2805,7 @@ extension on TranslationsNb {
'liveTv.recordEpisode' => 'Ta opp episode',
'liveTv.recordSeries' => 'Ta opp serie',
'liveTv.recordOptions' => 'Opptaksvalg',
'liveTv.saveTo' => 'Lagre i',
'liveTv.recordings' => 'Opptak',
'liveTv.scheduledRecordings' => 'Planlagt',
'liveTv.recordingRules' => 'Opptaksregler',
@@ -3020,9 +3022,9 @@ extension on TranslationsNb {
'downloads.noSyncRules' => 'Ingen synkroniseringsregler',
'downloads.manageSyncRule' => 'Administrer synkronisering',
'downloads.editEpisodeCount' => 'Antall episoder',
'downloads.editSyncFilter' => 'Synkroniseringsfilter',
_ => null,
} ?? switch (path) {
'downloads.editSyncFilter' => 'Synkroniseringsfilter',
'downloads.syncAllItems' => 'Synkroniserer alle elementer',
'downloads.syncUnwatchedItems' => 'Synkroniserer usette elementer',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}',
+3 -1
View File
@@ -1106,6 +1106,7 @@ class _TranslationsLiveTvNl extends TranslationsLiveTvEn {
@override String get recordEpisode => 'Aflevering opnemen';
@override String get recordSeries => 'Serie opnemen';
@override String get recordOptions => 'Opnameopties';
@override String get saveTo => 'Opslaan in';
@override String get recordings => 'Opnames';
@override String get scheduledRecordings => 'Gepland';
@override String get recordingRules => 'Opnameregels';
@@ -2804,6 +2805,7 @@ extension on TranslationsNl {
'liveTv.recordEpisode' => 'Aflevering opnemen',
'liveTv.recordSeries' => 'Serie opnemen',
'liveTv.recordOptions' => 'Opnameopties',
'liveTv.saveTo' => 'Opslaan in',
'liveTv.recordings' => 'Opnames',
'liveTv.scheduledRecordings' => 'Gepland',
'liveTv.recordingRules' => 'Opnameregels',
@@ -3020,9 +3022,9 @@ extension on TranslationsNl {
'downloads.noSyncRules' => 'Geen synchronisatieregels',
'downloads.manageSyncRule' => 'Synchronisatie beheren',
'downloads.editEpisodeCount' => 'Aantal afleveringen',
'downloads.editSyncFilter' => 'Synchronisatiefilter',
_ => null,
} ?? switch (path) {
'downloads.editSyncFilter' => 'Synchronisatiefilter',
'downloads.syncAllItems' => 'Alle items synchroniseren',
'downloads.syncUnwatchedItems' => 'Ongekeken items synchroniseren',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}',
+3 -1
View File
@@ -1106,6 +1106,7 @@ class _TranslationsLiveTvPl extends TranslationsLiveTvEn {
@override String get recordEpisode => 'Nagraj odcinek';
@override String get recordSeries => 'Nagraj serial';
@override String get recordOptions => 'Opcje nagrywania';
@override String get saveTo => 'Zapisz w';
@override String get recordings => 'Nagrania';
@override String get scheduledRecordings => 'Zaplanowane';
@override String get recordingRules => 'Reguły nagrywania';
@@ -2806,6 +2807,7 @@ extension on TranslationsPl {
'liveTv.recordEpisode' => 'Nagraj odcinek',
'liveTv.recordSeries' => 'Nagraj serial',
'liveTv.recordOptions' => 'Opcje nagrywania',
'liveTv.saveTo' => 'Zapisz w',
'liveTv.recordings' => 'Nagrania',
'liveTv.scheduledRecordings' => 'Zaplanowane',
'liveTv.recordingRules' => 'Reguły nagrywania',
@@ -3022,9 +3024,9 @@ extension on TranslationsPl {
'downloads.noSyncRules' => 'Brak reguł synchronizacji',
'downloads.manageSyncRule' => 'Zarządzaj synchronizacją',
'downloads.editEpisodeCount' => 'Liczba odcinków',
'downloads.editSyncFilter' => 'Filtr synchronizacji',
_ => null,
} ?? switch (path) {
'downloads.editSyncFilter' => 'Filtr synchronizacji',
'downloads.syncAllItems' => 'Synchronizuję wszystkie elementy',
'downloads.syncUnwatchedItems' => 'Synchronizuję nieobejrzane elementy',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Serwer: ${server}${status}',
+3 -1
View File
@@ -1106,6 +1106,7 @@ class _TranslationsLiveTvPt extends TranslationsLiveTvEn {
@override String get recordEpisode => 'Gravar episódio';
@override String get recordSeries => 'Gravar série';
@override String get recordOptions => 'Opções de gravação';
@override String get saveTo => 'Salvar em';
@override String get recordings => 'Gravações';
@override String get scheduledRecordings => 'Agendadas';
@override String get recordingRules => 'Regras de gravação';
@@ -2804,6 +2805,7 @@ extension on TranslationsPt {
'liveTv.recordEpisode' => 'Gravar episódio',
'liveTv.recordSeries' => 'Gravar série',
'liveTv.recordOptions' => 'Opções de gravação',
'liveTv.saveTo' => 'Salvar em',
'liveTv.recordings' => 'Gravações',
'liveTv.scheduledRecordings' => 'Agendadas',
'liveTv.recordingRules' => 'Regras de gravação',
@@ -3020,9 +3022,9 @@ extension on TranslationsPt {
'downloads.noSyncRules' => 'Nenhuma regra de sincronização',
'downloads.manageSyncRule' => 'Gerenciar sincronização',
'downloads.editEpisodeCount' => 'Número de episódios',
'downloads.editSyncFilter' => 'Filtro de sincronização',
_ => null,
} ?? switch (path) {
'downloads.editSyncFilter' => 'Filtro de sincronização',
'downloads.syncAllItems' => 'Sincronizando todos os itens',
'downloads.syncUnwatchedItems' => 'Sincronizando itens não vistos',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Servidor: ${server}${status}',
+3 -1
View File
@@ -1106,6 +1106,7 @@ class _TranslationsLiveTvRu extends TranslationsLiveTvEn {
@override String get recordEpisode => 'Записать эпизод';
@override String get recordSeries => 'Записать сериал';
@override String get recordOptions => 'Параметры записи';
@override String get saveTo => 'Сохранить в';
@override String get recordings => 'Записи';
@override String get scheduledRecordings => 'Запланировано';
@override String get recordingRules => 'Правила записи';
@@ -2806,6 +2807,7 @@ extension on TranslationsRu {
'liveTv.recordEpisode' => 'Записать эпизод',
'liveTv.recordSeries' => 'Записать сериал',
'liveTv.recordOptions' => 'Параметры записи',
'liveTv.saveTo' => 'Сохранить в',
'liveTv.recordings' => 'Записи',
'liveTv.scheduledRecordings' => 'Запланировано',
'liveTv.recordingRules' => 'Правила записи',
@@ -3022,9 +3024,9 @@ extension on TranslationsRu {
'downloads.noSyncRules' => 'Нет правил синхронизации',
'downloads.manageSyncRule' => 'Управление синхронизацией',
'downloads.editEpisodeCount' => 'Количество эпизодов',
'downloads.editSyncFilter' => 'Фильтр синхронизации',
_ => null,
} ?? switch (path) {
'downloads.editSyncFilter' => 'Фильтр синхронизации',
'downloads.syncAllItems' => 'Синхронизация всех элементов',
'downloads.syncUnwatchedItems' => 'Синхронизация непросмотренных элементов',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Сервер: ${server}${status}',
+3 -1
View File
@@ -1106,6 +1106,7 @@ class _TranslationsLiveTvSv extends TranslationsLiveTvEn {
@override String get recordEpisode => 'Spela in avsnitt';
@override String get recordSeries => 'Spela in serie';
@override String get recordOptions => 'Inspelningsalternativ';
@override String get saveTo => 'Spara till';
@override String get recordings => 'Inspelningar';
@override String get scheduledRecordings => 'Schemalagda';
@override String get recordingRules => 'Inspelningsregler';
@@ -2804,6 +2805,7 @@ extension on TranslationsSv {
'liveTv.recordEpisode' => 'Spela in avsnitt',
'liveTv.recordSeries' => 'Spela in serie',
'liveTv.recordOptions' => 'Inspelningsalternativ',
'liveTv.saveTo' => 'Spara till',
'liveTv.recordings' => 'Inspelningar',
'liveTv.scheduledRecordings' => 'Schemalagda',
'liveTv.recordingRules' => 'Inspelningsregler',
@@ -3020,9 +3022,9 @@ extension on TranslationsSv {
'downloads.noSyncRules' => 'Inga synkregler',
'downloads.manageSyncRule' => 'Hantera synkronisering',
'downloads.editEpisodeCount' => 'Antal avsnitt',
'downloads.editSyncFilter' => 'Synkroniseringsfilter',
_ => null,
} ?? switch (path) {
'downloads.editSyncFilter' => 'Synkroniseringsfilter',
'downloads.syncAllItems' => 'Synkroniserar alla objekt',
'downloads.syncUnwatchedItems' => 'Synkroniserar osedda objekt',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}',
+3 -1
View File
@@ -1106,6 +1106,7 @@ class _TranslationsLiveTvZh extends TranslationsLiveTvEn {
@override String get recordEpisode => '录制单集';
@override String get recordSeries => '录制整部剧集';
@override String get recordOptions => '录制选项';
@override String get saveTo => '保存到';
@override String get recordings => '录制内容';
@override String get scheduledRecordings => '已安排';
@override String get recordingRules => '录制规则';
@@ -2803,6 +2804,7 @@ extension on TranslationsZh {
'liveTv.recordEpisode' => '录制单集',
'liveTv.recordSeries' => '录制整部剧集',
'liveTv.recordOptions' => '录制选项',
'liveTv.saveTo' => '保存到',
'liveTv.recordings' => '录制内容',
'liveTv.scheduledRecordings' => '已安排',
'liveTv.recordingRules' => '录制规则',
@@ -3019,9 +3021,9 @@ extension on TranslationsZh {
'downloads.noSyncRules' => '没有同步规则',
'downloads.manageSyncRule' => '管理同步',
'downloads.editEpisodeCount' => '剧集数量',
'downloads.editSyncFilter' => '同步筛选',
_ => null,
} ?? switch (path) {
'downloads.editSyncFilter' => '同步筛选',
'downloads.syncAllItems' => '同步所有项目',
'downloads.syncUnwatchedItems' => '同步未观看项目',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => '服务器: ${server}${status}',
+1
View File
@@ -879,6 +879,7 @@
"recordEpisode": "Spela in avsnitt",
"recordSeries": "Spela in serie",
"recordOptions": "Inspelningsalternativ",
"saveTo": "Spara till",
"recordings": "Inspelningar",
"scheduledRecordings": "Schemalagda",
"recordingRules": "Inspelningsregler",
+1
View File
@@ -879,6 +879,7 @@
"recordEpisode": "录制单集",
"recordSeries": "录制整部剧集",
"recordOptions": "录制选项",
"saveTo": "保存到",
"recordings": "录制内容",
"scheduledRecordings": "已安排",
"recordingRules": "录制规则",
+7 -1
View File
@@ -190,9 +190,15 @@ class MediaSubscriptionCreateRequest {
for (final setting in subscription.settings)
if (setting.id.isNotEmpty) setting.id: setting.value ?? setting.defaultValue,
};
// The template's location id belongs to the template's section; when the
// caller redirects to another section it must not be sent along (the
// server then uses the new section's default location).
final overridingSection =
targetLibrarySectionID != null && targetLibrarySectionID != subscription.targetLibrarySectionID;
return MediaSubscriptionCreateRequest(
targetLibrarySectionID: targetLibrarySectionID ?? subscription.targetLibrarySectionID,
targetSectionLocationID: targetSectionLocationID ?? subscription.targetSectionLocationID,
targetSectionLocationID:
targetSectionLocationID ?? (overridingSection ? null : subscription.targetSectionLocationID),
type: subscription.type,
parameters: subscription.parameters,
prefs: {...templatePrefs, ...prefs},
+140 -31
View File
@@ -7,10 +7,14 @@ import '../../focus/focusable_button.dart';
import '../../focus/focusable_text_field.dart';
import '../../focus/focusable_wrapper.dart';
import '../../i18n/strings.g.dart';
import '../../media/media_kind.dart';
import '../../media/media_library.dart';
import '../../media/media_server_client.dart';
import '../../mixins/controller_disposer_mixin.dart';
import '../../models/livetv_program.dart';
import '../../models/media_subscription.dart';
import '../../services/plex_constants.dart';
import '../../services/settings_service.dart';
import '../../utils/app_logger.dart';
import '../../utils/dialogs.dart';
import '../../widgets/app_icon.dart';
@@ -87,6 +91,8 @@ class _RecordOptionsContent extends StatefulWidget {
class _RecordOptionsContentState extends State<_RecordOptionsContent> {
late MediaSubscription _entry;
final Map<String, Object?> _dirtyPrefs = {};
List<MediaLibrary> _libraries = const [];
int? _selectedSectionId;
bool _saving = false;
final _saveFocusNode = FocusNode(debugLabel: 'record_options_save');
@@ -94,6 +100,7 @@ class _RecordOptionsContentState extends State<_RecordOptionsContent> {
void initState() {
super.initState();
_entry = widget.entries.firstWhere((e) => e.selected == true, orElse: () => widget.entries.first);
if (!widget.isEdit) _loadLibraries();
}
@override
@@ -102,11 +109,60 @@ class _RecordOptionsContentState extends State<_RecordOptionsContent> {
super.dispose();
}
Future<void> _loadLibraries() async {
try {
final libraries = await widget.client.fetchLibraries();
if (mounted) setState(() => _libraries = libraries);
} catch (e) {
// Non-fatal: the row degrades to a read-only template default.
appLogger.w('Failed to fetch libraries for recording target picker', error: e);
}
}
/// Library kind a recording of [_entry]'s type lands in, or null when no
/// sensible mapping exists (no picker shown).
MediaKind? get _entryLibraryKind => switch (_entry.type) {
PlexMetadataType.movie => MediaKind.movie,
PlexMetadataType.show || PlexMetadataType.season || PlexMetadataType.episode => MediaKind.show,
_ => null,
};
/// Sections eligible as recording targets for the selected entry.
List<MediaLibrary> get _eligibleLibraries {
final kind = _entryLibraryKind;
if (kind == null) return const [];
return [
for (final library in _libraries)
if (library.kind == kind && !library.isShared && int.tryParse(library.id) != null) library,
];
}
/// Last explicitly picked target for this server + entry type, if it still
/// refers to an eligible section.
int? _savedSectionId(List<MediaLibrary> eligible) {
final type = _entry.type;
if (type == null) return null;
final saved =
SettingsService.instanceOrNull?.read(SettingsService.dvrTargetSectionPref(widget.client.serverId, type)) ?? 0;
if (saved <= 0) return null;
return eligible.any((library) => library.id == '$saved') ? saved : null;
}
int? _effectiveSectionId(List<MediaLibrary> eligible) =>
_selectedSectionId ?? _savedSectionId(eligible) ?? _entry.targetLibrarySectionID;
String _targetLibraryLabel(List<MediaLibrary> eligible) {
final target = _effectiveSectionId(eligible);
final match = eligible.where((library) => library.id == '$target').firstOrNull;
return match?.title ?? _entry.librarySectionTitle ?? '';
}
void _selectEntry(MediaSubscription entry) {
if (identical(entry, _entry)) return;
setState(() {
_entry = entry;
_dirtyPrefs.clear();
_selectedSectionId = null;
});
}
@@ -134,7 +190,8 @@ class _RecordOptionsContentState extends State<_RecordOptionsContent> {
}
Future<void> _save() async {
if (!widget.isEdit && _entry.targetLibrarySectionID == null) {
final targetSectionId = widget.isEdit ? null : _effectiveSectionId(_eligibleLibraries);
if (!widget.isEdit && targetSectionId == null) {
_close(RecordOutcome.targetMissing);
return;
}
@@ -150,8 +207,13 @@ class _RecordOptionsContentState extends State<_RecordOptionsContent> {
if (!mounted) return;
_close(RecordOutcome.updated);
} else {
final request = MediaSubscriptionCreateRequest.fromTemplate(_entry, prefs: Map.of(_dirtyPrefs));
final request = MediaSubscriptionCreateRequest.fromTemplate(
_entry,
prefs: Map.of(_dirtyPrefs),
targetLibrarySectionID: targetSectionId,
);
await widget.client.liveTv.createRecordingRule(request);
_persistTargetPick();
if (!mounted) return;
_close(RecordOutcome.scheduled);
}
@@ -168,14 +230,35 @@ class _RecordOptionsContentState extends State<_RecordOptionsContent> {
}
}
/// Remember an explicit library pick (never the template default, so the
/// server default keeps applying until the user chooses).
void _persistTargetPick() {
final picked = _selectedSectionId;
final type = _entry.type;
if (picked == null || type == null) return;
SettingsService.instanceOrNull?.write(SettingsService.dvrTargetSectionPref(widget.client.serverId, type), picked);
}
void _close([RecordOutcome? result]) {
OverlaySheetController.popAdaptive(context, result ?? RecordOutcome.cancelled);
}
Future<void> _openTargetPicker(List<MediaLibrary> eligible) async {
final selected = await showOptionPickerDialog<int>(
context,
title: t.liveTv.saveTo,
options: [for (final library in eligible) (icon: null, label: library.title, value: int.parse(library.id))],
);
if (selected != null && mounted) setState(() => _selectedSectionId = selected);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final visibleSettings = _entry.settings.where((s) => !s.hidden && !s.advanced).toList();
final eligibleLibraries = widget.isEdit ? const <MediaLibrary>[] : _eligibleLibraries;
final targetLabel = widget.isEdit ? (_entry.librarySectionTitle ?? '') : _targetLibraryLabel(eligibleLibraries);
final showTargetRow = targetLabel.isNotEmpty || eligibleLibraries.length > 1;
return Padding(
padding: const EdgeInsets.all(20),
@@ -209,6 +292,14 @@ class _RecordOptionsContentState extends State<_RecordOptionsContent> {
const SizedBox(height: 12),
_EntryChooser(entries: widget.entries, selected: _entry, onSelect: _selectEntry),
],
if (showTargetRow) ...[
const SizedBox(height: 8),
_PickerRow(
label: t.liveTv.saveTo,
value: targetLabel,
onTap: eligibleLibraries.length > 1 ? () => _openTargetPicker(eligibleLibraries) : null,
),
],
const SizedBox(height: 8),
Flexible(
child: visibleSettings.isEmpty
@@ -416,42 +507,60 @@ class _EnumSettingRow extends StatelessWidget {
return match?.label ?? str;
}
@override
Widget build(BuildContext context) {
return _PickerRow(
label: setting.label ?? setting.id,
summary: setting.summary,
value: _labelForValue(currentValue),
autofocus: autofocus,
onTap: () => _openPicker(context),
);
}
}
/// Label / value / chevron row that opens a picker on tap. Renders as a
/// static, non-focusable row when [onTap] is null.
class _PickerRow extends StatelessWidget {
final String label;
final String? summary;
final String value;
final bool autofocus;
final VoidCallback? onTap;
const _PickerRow({required this.label, this.summary, required this.value, this.autofocus = false, this.onTap});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final onTap = this.onTap;
final row = Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: .start,
children: [
Text(label, style: theme.textTheme.bodyMedium),
if (summary != null && summary!.isNotEmpty)
Text(summary!, style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant)),
],
),
),
const SizedBox(width: 12),
Text(value, style: theme.textTheme.bodyMedium),
if (onTap != null) ...[const SizedBox(width: 4), const AppIcon(Symbols.chevron_right_rounded)],
],
),
);
if (onTap == null) return row;
return FocusableWrapper(
autofocus: autofocus,
autoScroll: true,
useBackgroundFocus: true,
onSelect: () => _openPicker(context),
child: InkWell(
canRequestFocus: false,
onTap: () => _openPicker(context),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: .start,
children: [
Text(setting.label ?? setting.id, style: theme.textTheme.bodyMedium),
if (setting.summary != null && setting.summary!.isNotEmpty)
Text(
setting.summary!,
style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant),
),
],
),
),
const SizedBox(width: 12),
Text(_labelForValue(currentValue), style: theme.textTheme.bodyMedium),
const SizedBox(width: 4),
const AppIcon(Symbols.chevron_right_rounded),
],
),
),
),
onSelect: onTap,
child: InkWell(canRequestFocus: false, onTap: onTap, child: row),
);
}
}
+6
View File
@@ -570,6 +570,12 @@ class SettingsService extends BaseSharedPreferencesService {
static IntPref watchedThresholdPref(ServerId serverId) => IntPref('watched_threshold_$serverId', defaultValue: 90);
/// Library section the user last picked as a DVR recording target, keyed by
/// subscription type (movie/show) so the two don't clobber each other.
/// 0 = unset (only explicit picks are written; the server template default
/// keeps applying until the user chooses).
static IntPref dvrTargetSectionPref(ServerId serverId, int type) => IntPref('dvr_target_section_${type}_$serverId');
static EnumPref<TrackerLibraryFilterMode> trackerFilterModePref(TrackerService s) => EnumPref(
'tracker_library_filter_mode_${s.name}',
values: TrackerLibraryFilterMode.values,
@@ -215,6 +215,43 @@ void main() {
expect(created?.key, '18');
});
test('fromTemplate section override drops the template location id', () async {
late http.Request captured;
final client = makeClient((request) async {
captured = request;
return jsonResponse({
'MediaContainer': {
'MediaSubscription': [
{'key': '18', 'type': 4, 'title': 'Episode'},
],
},
});
});
addTearDown(client.close);
const template = MediaSubscription(key: '', targetLibrarySectionID: 2, targetSectionLocationID: 7, type: 4);
final overridden = MediaSubscriptionCreateRequest.fromTemplate(template, targetLibrarySectionID: 5);
expect(overridden.targetLibrarySectionID, 5);
expect(overridden.targetSectionLocationID, isNull);
await client.liveTv.createRecordingRule(overridden);
expect(captured.url.queryParameters['targetLibrarySectionID'], '5');
expect(captured.url.queryParameters.containsKey('targetSectionLocationID'), isFalse);
});
test('fromTemplate without override keeps template section and location', () {
const template = MediaSubscription(key: '', targetLibrarySectionID: 2, targetSectionLocationID: 7, type: 4);
final request = MediaSubscriptionCreateRequest.fromTemplate(template);
expect(request.targetLibrarySectionID, 2);
expect(request.targetSectionLocationID, 7);
// Same-section override is a no-op: the template location still applies.
final sameSection = MediaSubscriptionCreateRequest.fromTemplate(template, targetLibrarySectionID: 2);
expect(sameSection.targetSectionLocationID, 7);
});
test('updateRecordingRule sends prefs as query params', () async {
late http.Request captured;
final client = makeClient((request) async {