From 632d2ec16b0a2252aa78e84f8c57cf579733c84b Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Thu, 9 Jul 2026 21:31:19 +0200 Subject: [PATCH] feat(livetv): let user pick and remember the recording target library close #1513 --- lib/i18n/bg.i18n.json | 1 + lib/i18n/da.i18n.json | 1 + lib/i18n/de.i18n.json | 1 + lib/i18n/en.i18n.json | 1 + lib/i18n/es.i18n.json | 1 + lib/i18n/fr.i18n.json | 1 + lib/i18n/it.i18n.json | 1 + lib/i18n/ja.i18n.json | 1 + lib/i18n/ko.i18n.json | 1 + lib/i18n/nb.i18n.json | 1 + lib/i18n/nl.i18n.json | 1 + lib/i18n/pl.i18n.json | 1 + lib/i18n/pt.i18n.json | 1 + lib/i18n/ru.i18n.json | 1 + lib/i18n/strings.g.dart | 2 +- lib/i18n/strings_bg.g.dart | 4 +- lib/i18n/strings_da.g.dart | 4 +- lib/i18n/strings_de.g.dart | 4 +- lib/i18n/strings_en.g.dart | 6 +- lib/i18n/strings_es.g.dart | 4 +- lib/i18n/strings_fr.g.dart | 4 +- lib/i18n/strings_it.g.dart | 4 +- lib/i18n/strings_ja.g.dart | 4 +- lib/i18n/strings_ko.g.dart | 4 +- lib/i18n/strings_nb.g.dart | 4 +- lib/i18n/strings_nl.g.dart | 4 +- lib/i18n/strings_pl.g.dart | 4 +- lib/i18n/strings_pt.g.dart | 4 +- lib/i18n/strings_ru.g.dart | 4 +- lib/i18n/strings_sv.g.dart | 4 +- lib/i18n/strings_zh.g.dart | 4 +- lib/i18n/sv.i18n.json | 1 + lib/i18n/zh.i18n.json | 1 + lib/models/media_subscription.dart | 8 +- lib/screens/livetv/record_options_sheet.dart | 171 +++++++++++++++---- lib/services/settings_service.dart | 6 + test/services/plex_live_tv_support_test.dart | 37 ++++ 37 files changed, 257 insertions(+), 49 deletions(-) diff --git a/lib/i18n/bg.i18n.json b/lib/i18n/bg.i18n.json index b3099d5c..63641104 100644 --- a/lib/i18n/bg.i18n.json +++ b/lib/i18n/bg.i18n.json @@ -879,6 +879,7 @@ "recordEpisode": "Запиши епизод", "recordSeries": "Запиши сериал", "recordOptions": "Опции за запис", + "saveTo": "Запази в", "recordings": "Записи", "scheduledRecordings": "Планирани", "recordingRules": "Правила за запис", diff --git a/lib/i18n/da.i18n.json b/lib/i18n/da.i18n.json index 358f1e8a..0cf7e4e5 100644 --- a/lib/i18n/da.i18n.json +++ b/lib/i18n/da.i18n.json @@ -879,6 +879,7 @@ "recordEpisode": "Optag episode", "recordSeries": "Optag serie", "recordOptions": "Optageindstillinger", + "saveTo": "Gem i", "recordings": "Optagelser", "scheduledRecordings": "Planlagt", "recordingRules": "Optagelsesregler", diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index 0a71f5f2..13a6b087 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -879,6 +879,7 @@ "recordEpisode": "Episode aufnehmen", "recordSeries": "Serie aufnehmen", "recordOptions": "Aufnahmeoptionen", + "saveTo": "Speichern in", "recordings": "Aufnahmen", "scheduledRecordings": "Geplant", "recordingRules": "Aufnahmeregeln", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index d444967d..0770a903 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -891,6 +891,7 @@ "recordEpisode": "Record Episode", "recordSeries": "Record Series", "recordOptions": "Record Options", + "saveTo": "Save to", "recordings": "Recordings", "scheduledRecordings": "Scheduled", "recordingRules": "Recording Rules", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index 1f2177d7..30330f9c 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -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", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index a0c7588e..cae0cf4f 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -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", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 808d2c35..c80ab341 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -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", diff --git a/lib/i18n/ja.i18n.json b/lib/i18n/ja.i18n.json index 9bdc4c79..56d29cd1 100644 --- a/lib/i18n/ja.i18n.json +++ b/lib/i18n/ja.i18n.json @@ -879,6 +879,7 @@ "recordEpisode": "このエピソードを録画", "recordSeries": "シリーズを録画", "recordOptions": "録画オプション", + "saveTo": "保存先", "recordings": "録画", "scheduledRecordings": "予約", "recordingRules": "録画ルール", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index efd390e8..01f27493 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -879,6 +879,7 @@ "recordEpisode": "에피소드 녹화", "recordSeries": "시리즈 녹화", "recordOptions": "녹화 옵션", + "saveTo": "저장 위치", "recordings": "녹화", "scheduledRecordings": "예약됨", "recordingRules": "녹화 규칙", diff --git a/lib/i18n/nb.i18n.json b/lib/i18n/nb.i18n.json index b4d1f475..8d6672b6 100644 --- a/lib/i18n/nb.i18n.json +++ b/lib/i18n/nb.i18n.json @@ -879,6 +879,7 @@ "recordEpisode": "Ta opp episode", "recordSeries": "Ta opp serie", "recordOptions": "Opptaksvalg", + "saveTo": "Lagre i", "recordings": "Opptak", "scheduledRecordings": "Planlagt", "recordingRules": "Opptaksregler", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index d5791529..43730ef6 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -879,6 +879,7 @@ "recordEpisode": "Aflevering opnemen", "recordSeries": "Serie opnemen", "recordOptions": "Opnameopties", + "saveTo": "Opslaan in", "recordings": "Opnames", "scheduledRecordings": "Gepland", "recordingRules": "Opnameregels", diff --git a/lib/i18n/pl.i18n.json b/lib/i18n/pl.i18n.json index 82f5670c..b7d48cbd 100644 --- a/lib/i18n/pl.i18n.json +++ b/lib/i18n/pl.i18n.json @@ -879,6 +879,7 @@ "recordEpisode": "Nagraj odcinek", "recordSeries": "Nagraj serial", "recordOptions": "Opcje nagrywania", + "saveTo": "Zapisz w", "recordings": "Nagrania", "scheduledRecordings": "Zaplanowane", "recordingRules": "Reguły nagrywania", diff --git a/lib/i18n/pt.i18n.json b/lib/i18n/pt.i18n.json index 6567b16e..822e7e62 100644 --- a/lib/i18n/pt.i18n.json +++ b/lib/i18n/pt.i18n.json @@ -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", diff --git a/lib/i18n/ru.i18n.json b/lib/i18n/ru.i18n.json index 612332ea..e0057752 100644 --- a/lib/i18n/ru.i18n.json +++ b/lib/i18n/ru.i18n.json @@ -879,6 +879,7 @@ "recordEpisode": "Записать эпизод", "recordSeries": "Записать сериал", "recordOptions": "Параметры записи", + "saveTo": "Сохранить в", "recordings": "Записи", "scheduledRecordings": "Запланировано", "recordingRules": "Правила записи", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index e166a58e..d22cc91b 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -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 diff --git a/lib/i18n/strings_bg.g.dart b/lib/i18n/strings_bg.g.dart index 4b5aa30d..4c793b03 100644 --- a/lib/i18n/strings_bg.g.dart +++ b/lib/i18n/strings_bg.g.dart @@ -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}', diff --git a/lib/i18n/strings_da.g.dart b/lib/i18n/strings_da.g.dart index 4d82808e..73b04dec 100644 --- a/lib/i18n/strings_da.g.dart +++ b/lib/i18n/strings_da.g.dart @@ -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}', diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index 511ed3b8..afe78dba 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -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}', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index bbb5c1c3..ddf97d87 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -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', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index 08c4a661..5e22d6e8 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -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}', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index 43adec54..3bc19e0f 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -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}', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index 9a36baef..cfd48ee4 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -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}', diff --git a/lib/i18n/strings_ja.g.dart b/lib/i18n/strings_ja.g.dart index 45ea642d..6881d4c7 100644 --- a/lib/i18n/strings_ja.g.dart +++ b/lib/i18n/strings_ja.g.dart @@ -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}', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index d5954883..fc6ec963 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -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}', diff --git a/lib/i18n/strings_nb.g.dart b/lib/i18n/strings_nb.g.dart index 9e24344e..5f65d9bd 100644 --- a/lib/i18n/strings_nb.g.dart +++ b/lib/i18n/strings_nb.g.dart @@ -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}', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index 14754ea6..421d298b 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -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}', diff --git a/lib/i18n/strings_pl.g.dart b/lib/i18n/strings_pl.g.dart index b08a5db9..91f3d4d9 100644 --- a/lib/i18n/strings_pl.g.dart +++ b/lib/i18n/strings_pl.g.dart @@ -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}', diff --git a/lib/i18n/strings_pt.g.dart b/lib/i18n/strings_pt.g.dart index 7a57ed07..e9b1b2a9 100644 --- a/lib/i18n/strings_pt.g.dart +++ b/lib/i18n/strings_pt.g.dart @@ -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}', diff --git a/lib/i18n/strings_ru.g.dart b/lib/i18n/strings_ru.g.dart index 87018dc2..19fd93e4 100644 --- a/lib/i18n/strings_ru.g.dart +++ b/lib/i18n/strings_ru.g.dart @@ -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}', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index 5969d9fd..05e71591 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -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}', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index 2392675a..23e0853c 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -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}', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 10c8fcfd..b95dc1de 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -879,6 +879,7 @@ "recordEpisode": "Spela in avsnitt", "recordSeries": "Spela in serie", "recordOptions": "Inspelningsalternativ", + "saveTo": "Spara till", "recordings": "Inspelningar", "scheduledRecordings": "Schemalagda", "recordingRules": "Inspelningsregler", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index fecf8069..f6dea236 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -879,6 +879,7 @@ "recordEpisode": "录制单集", "recordSeries": "录制整部剧集", "recordOptions": "录制选项", + "saveTo": "保存到", "recordings": "录制内容", "scheduledRecordings": "已安排", "recordingRules": "录制规则", diff --git a/lib/models/media_subscription.dart b/lib/models/media_subscription.dart index 72ec137b..23763bf4 100644 --- a/lib/models/media_subscription.dart +++ b/lib/models/media_subscription.dart @@ -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}, diff --git a/lib/screens/livetv/record_options_sheet.dart b/lib/screens/livetv/record_options_sheet.dart index 71d42d45..541fe1d2 100644 --- a/lib/screens/livetv/record_options_sheet.dart +++ b/lib/screens/livetv/record_options_sheet.dart @@ -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 _dirtyPrefs = {}; + List _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 _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 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 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 eligible) => + _selectedSectionId ?? _savedSectionId(eligible) ?? _entry.targetLibrarySectionID; + + String _targetLibraryLabel(List 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 _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 _openTargetPicker(List eligible) async { + final selected = await showOptionPickerDialog( + 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 [] : _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), ); } } diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 0b22e9a2..926100f0 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -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 trackerFilterModePref(TrackerService s) => EnumPref( 'tracker_library_filter_mode_${s.name}', values: TrackerLibraryFilterMode.values, diff --git a/test/services/plex_live_tv_support_test.dart b/test/services/plex_live_tv_support_test.dart index 70088215..525026a6 100644 --- a/test/services/plex_live_tv_support_test.dart +++ b/test/services/plex_live_tv_support_test.dart @@ -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 {