diff --git a/lib/database/app_database.dart b/lib/database/app_database.dart index 81256885..faf14c49 100644 --- a/lib/database/app_database.dart +++ b/lib/database/app_database.dart @@ -61,7 +61,7 @@ class AppDatabase extends _$AppDatabase { AppDatabase.forTesting(super.e); @override - int get schemaVersion => 15; + int get schemaVersion => 16; @override MigrationStrategy get migration { @@ -214,6 +214,13 @@ class AppDatabase extends _$AppDatabase { () => m.addColumn(downloadedMedia, downloadedMedia.mediaSourceId), ); } + if (from < 16) { + appLogger.i('Adding includeSpecials column to SyncRules (v16 migration)'); + await _ignoreAlreadyExists( + 'SyncRules.includeSpecials column', + () => m.addColumn(syncRules, syncRules.includeSpecials), + ); + } }, ); } @@ -532,6 +539,7 @@ class AppDatabase extends _$AppDatabase { required int episodeCount, int mediaIndex = 0, String downloadFilter = 'unwatched', + bool includeSpecials = true, }) async { // [insertOnConflictUpdate] defaults the conflict target to the primary // key (`id`), which is auto-incremented — the conflict never triggers @@ -549,6 +557,7 @@ class AppDatabase extends _$AppDatabase { createdAt: DateTime.now().millisecondsSinceEpoch, mediaIndex: Value(mediaIndex), downloadFilter: Value(downloadFilter), + includeSpecials: Value(includeSpecials), ), onConflict: DoUpdate( (_) => SyncRulesCompanion( @@ -559,6 +568,7 @@ class AppDatabase extends _$AppDatabase { episodeCount: Value(episodeCount), mediaIndex: Value(mediaIndex), downloadFilter: Value(downloadFilter), + includeSpecials: Value(includeSpecials), ), target: [syncRules.globalKey], ), diff --git a/lib/database/app_database.g.dart b/lib/database/app_database.g.dart index ae9fcca1..3e3658b1 100644 --- a/lib/database/app_database.g.dart +++ b/lib/database/app_database.g.dart @@ -3150,6 +3150,21 @@ class $SyncRulesTable extends SyncRules requiredDuringInsert: false, defaultValue: const Constant('unwatched'), ); + static const VerificationMeta _includeSpecialsMeta = const VerificationMeta( + 'includeSpecials', + ); + @override + late final GeneratedColumn includeSpecials = GeneratedColumn( + 'include_specials', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("include_specials" IN (0, 1))', + ), + defaultValue: const Constant(true), + ); @override List get $columns => [ id, @@ -3164,6 +3179,7 @@ class $SyncRulesTable extends SyncRules lastExecutedAt, mediaIndex, downloadFilter, + includeSpecials, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -3267,6 +3283,15 @@ class $SyncRulesTable extends SyncRules ), ); } + if (data.containsKey('include_specials')) { + context.handle( + _includeSpecialsMeta, + includeSpecials.isAcceptableOrUnknown( + data['include_specials']!, + _includeSpecialsMeta, + ), + ); + } return context; } @@ -3324,6 +3349,10 @@ class $SyncRulesTable extends SyncRules DriftSqlType.string, data['${effectivePrefix}download_filter'], )!, + includeSpecials: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}include_specials'], + )!, ); } @@ -3346,6 +3375,7 @@ class SyncRuleItem extends DataClass implements Insertable { final int? lastExecutedAt; final int mediaIndex; final String downloadFilter; + final bool includeSpecials; const SyncRuleItem({ required this.id, required this.profileId, @@ -3359,6 +3389,7 @@ class SyncRuleItem extends DataClass implements Insertable { this.lastExecutedAt, required this.mediaIndex, required this.downloadFilter, + required this.includeSpecials, }); @override Map toColumns(bool nullToAbsent) { @@ -3377,6 +3408,7 @@ class SyncRuleItem extends DataClass implements Insertable { } map['media_index'] = Variable(mediaIndex); map['download_filter'] = Variable(downloadFilter); + map['include_specials'] = Variable(includeSpecials); return map; } @@ -3396,6 +3428,7 @@ class SyncRuleItem extends DataClass implements Insertable { : Value(lastExecutedAt), mediaIndex: Value(mediaIndex), downloadFilter: Value(downloadFilter), + includeSpecials: Value(includeSpecials), ); } @@ -3417,6 +3450,7 @@ class SyncRuleItem extends DataClass implements Insertable { lastExecutedAt: serializer.fromJson(json['lastExecutedAt']), mediaIndex: serializer.fromJson(json['mediaIndex']), downloadFilter: serializer.fromJson(json['downloadFilter']), + includeSpecials: serializer.fromJson(json['includeSpecials']), ); } @override @@ -3435,6 +3469,7 @@ class SyncRuleItem extends DataClass implements Insertable { 'lastExecutedAt': serializer.toJson(lastExecutedAt), 'mediaIndex': serializer.toJson(mediaIndex), 'downloadFilter': serializer.toJson(downloadFilter), + 'includeSpecials': serializer.toJson(includeSpecials), }; } @@ -3451,6 +3486,7 @@ class SyncRuleItem extends DataClass implements Insertable { Value lastExecutedAt = const Value.absent(), int? mediaIndex, String? downloadFilter, + bool? includeSpecials, }) => SyncRuleItem( id: id ?? this.id, profileId: profileId ?? this.profileId, @@ -3466,6 +3502,7 @@ class SyncRuleItem extends DataClass implements Insertable { : this.lastExecutedAt, mediaIndex: mediaIndex ?? this.mediaIndex, downloadFilter: downloadFilter ?? this.downloadFilter, + includeSpecials: includeSpecials ?? this.includeSpecials, ); SyncRuleItem copyWithCompanion(SyncRulesCompanion data) { return SyncRuleItem( @@ -3491,6 +3528,9 @@ class SyncRuleItem extends DataClass implements Insertable { downloadFilter: data.downloadFilter.present ? data.downloadFilter.value : this.downloadFilter, + includeSpecials: data.includeSpecials.present + ? data.includeSpecials.value + : this.includeSpecials, ); } @@ -3508,7 +3548,8 @@ class SyncRuleItem extends DataClass implements Insertable { ..write('createdAt: $createdAt, ') ..write('lastExecutedAt: $lastExecutedAt, ') ..write('mediaIndex: $mediaIndex, ') - ..write('downloadFilter: $downloadFilter') + ..write('downloadFilter: $downloadFilter, ') + ..write('includeSpecials: $includeSpecials') ..write(')')) .toString(); } @@ -3527,6 +3568,7 @@ class SyncRuleItem extends DataClass implements Insertable { lastExecutedAt, mediaIndex, downloadFilter, + includeSpecials, ); @override bool operator ==(Object other) => @@ -3543,7 +3585,8 @@ class SyncRuleItem extends DataClass implements Insertable { other.createdAt == this.createdAt && other.lastExecutedAt == this.lastExecutedAt && other.mediaIndex == this.mediaIndex && - other.downloadFilter == this.downloadFilter); + other.downloadFilter == this.downloadFilter && + other.includeSpecials == this.includeSpecials); } class SyncRulesCompanion extends UpdateCompanion { @@ -3559,6 +3602,7 @@ class SyncRulesCompanion extends UpdateCompanion { final Value lastExecutedAt; final Value mediaIndex; final Value downloadFilter; + final Value includeSpecials; const SyncRulesCompanion({ this.id = const Value.absent(), this.profileId = const Value.absent(), @@ -3572,6 +3616,7 @@ class SyncRulesCompanion extends UpdateCompanion { this.lastExecutedAt = const Value.absent(), this.mediaIndex = const Value.absent(), this.downloadFilter = const Value.absent(), + this.includeSpecials = const Value.absent(), }); SyncRulesCompanion.insert({ this.id = const Value.absent(), @@ -3586,6 +3631,7 @@ class SyncRulesCompanion extends UpdateCompanion { this.lastExecutedAt = const Value.absent(), this.mediaIndex = const Value.absent(), this.downloadFilter = const Value.absent(), + this.includeSpecials = const Value.absent(), }) : serverId = Value(serverId), ratingKey = Value(ratingKey), globalKey = Value(globalKey), @@ -3605,6 +3651,7 @@ class SyncRulesCompanion extends UpdateCompanion { Expression? lastExecutedAt, Expression? mediaIndex, Expression? downloadFilter, + Expression? includeSpecials, }) { return RawValuesInsertable({ if (id != null) 'id': id, @@ -3619,6 +3666,7 @@ class SyncRulesCompanion extends UpdateCompanion { if (lastExecutedAt != null) 'last_executed_at': lastExecutedAt, if (mediaIndex != null) 'media_index': mediaIndex, if (downloadFilter != null) 'download_filter': downloadFilter, + if (includeSpecials != null) 'include_specials': includeSpecials, }); } @@ -3635,6 +3683,7 @@ class SyncRulesCompanion extends UpdateCompanion { Value? lastExecutedAt, Value? mediaIndex, Value? downloadFilter, + Value? includeSpecials, }) { return SyncRulesCompanion( id: id ?? this.id, @@ -3649,6 +3698,7 @@ class SyncRulesCompanion extends UpdateCompanion { lastExecutedAt: lastExecutedAt ?? this.lastExecutedAt, mediaIndex: mediaIndex ?? this.mediaIndex, downloadFilter: downloadFilter ?? this.downloadFilter, + includeSpecials: includeSpecials ?? this.includeSpecials, ); } @@ -3691,6 +3741,9 @@ class SyncRulesCompanion extends UpdateCompanion { if (downloadFilter.present) { map['download_filter'] = Variable(downloadFilter.value); } + if (includeSpecials.present) { + map['include_specials'] = Variable(includeSpecials.value); + } return map; } @@ -3708,7 +3761,8 @@ class SyncRulesCompanion extends UpdateCompanion { ..write('createdAt: $createdAt, ') ..write('lastExecutedAt: $lastExecutedAt, ') ..write('mediaIndex: $mediaIndex, ') - ..write('downloadFilter: $downloadFilter') + ..write('downloadFilter: $downloadFilter, ') + ..write('includeSpecials: $includeSpecials') ..write(')')) .toString(); } @@ -6842,6 +6896,7 @@ typedef $$SyncRulesTableCreateCompanionBuilder = Value lastExecutedAt, Value mediaIndex, Value downloadFilter, + Value includeSpecials, }); typedef $$SyncRulesTableUpdateCompanionBuilder = SyncRulesCompanion Function({ @@ -6857,6 +6912,7 @@ typedef $$SyncRulesTableUpdateCompanionBuilder = Value lastExecutedAt, Value mediaIndex, Value downloadFilter, + Value includeSpecials, }); class $$SyncRulesTableFilterComposer @@ -6927,6 +6983,11 @@ class $$SyncRulesTableFilterComposer column: $table.downloadFilter, builder: (column) => ColumnFilters(column), ); + + ColumnFilters get includeSpecials => $composableBuilder( + column: $table.includeSpecials, + builder: (column) => ColumnFilters(column), + ); } class $$SyncRulesTableOrderingComposer @@ -6997,6 +7058,11 @@ class $$SyncRulesTableOrderingComposer column: $table.downloadFilter, builder: (column) => ColumnOrderings(column), ); + + ColumnOrderings get includeSpecials => $composableBuilder( + column: $table.includeSpecials, + builder: (column) => ColumnOrderings(column), + ); } class $$SyncRulesTableAnnotationComposer @@ -7053,6 +7119,11 @@ class $$SyncRulesTableAnnotationComposer column: $table.downloadFilter, builder: (column) => column, ); + + GeneratedColumn get includeSpecials => $composableBuilder( + column: $table.includeSpecials, + builder: (column) => column, + ); } class $$SyncRulesTableTableManager @@ -7098,6 +7169,7 @@ class $$SyncRulesTableTableManager Value lastExecutedAt = const Value.absent(), Value mediaIndex = const Value.absent(), Value downloadFilter = const Value.absent(), + Value includeSpecials = const Value.absent(), }) => SyncRulesCompanion( id: id, profileId: profileId, @@ -7111,6 +7183,7 @@ class $$SyncRulesTableTableManager lastExecutedAt: lastExecutedAt, mediaIndex: mediaIndex, downloadFilter: downloadFilter, + includeSpecials: includeSpecials, ), createCompanionCallback: ({ @@ -7126,6 +7199,7 @@ class $$SyncRulesTableTableManager Value lastExecutedAt = const Value.absent(), Value mediaIndex = const Value.absent(), Value downloadFilter = const Value.absent(), + Value includeSpecials = const Value.absent(), }) => SyncRulesCompanion.insert( id: id, profileId: profileId, @@ -7139,6 +7213,7 @@ class $$SyncRulesTableTableManager lastExecutedAt: lastExecutedAt, mediaIndex: mediaIndex, downloadFilter: downloadFilter, + includeSpecials: includeSpecials, ), withReferenceMapper: (p0) => p0 .map((e) => (e.readTable(table), BaseReferences(db, table, e))) diff --git a/lib/database/tables.dart b/lib/database/tables.dart index df3e14cb..ca4c60e9 100644 --- a/lib/database/tables.dart +++ b/lib/database/tables.dart @@ -101,6 +101,7 @@ class SyncRules extends Table { IntColumn get lastExecutedAt => integer().nullable()(); IntColumn get mediaIndex => integer().withDefault(const Constant(0))(); TextColumn get downloadFilter => text().withDefault(const Constant('unwatched'))(); + BoolColumn get includeSpecials => boolean().withDefault(const Constant(true))(); } /// Persisted media-server connections. diff --git a/lib/i18n/bg.i18n.json b/lib/i18n/bg.i18n.json index 74e05c79..5d1919db 100644 --- a/lib/i18n/bg.i18n.json +++ b/lib/i18n/bg.i18n.json @@ -1031,6 +1031,7 @@ "unwatchedOnly": "Само негледани", "nextNUnwatched": "Следващите ${count} негледани", "customAmount": "Персонален брой...", + "includeSpecials": "Включи специалните", "howManyEpisodes": "Колко епизода?", "itemsQueued": "${count} елемента са добавени в опашката за изтегляне", "keepSynced": "Поддържай синхронизирано", diff --git a/lib/i18n/da.i18n.json b/lib/i18n/da.i18n.json index dd9e787a..72e7b8c6 100644 --- a/lib/i18n/da.i18n.json +++ b/lib/i18n/da.i18n.json @@ -1031,6 +1031,7 @@ "unwatchedOnly": "Kun usete", "nextNUnwatched": "Næste ${count} usete", "customAmount": "Angiv antal...", + "includeSpecials": "Inkludér specials", "howManyEpisodes": "Hvor mange episoder?", "itemsQueued": "${count} elementer sat i kø til download", "keepSynced": "Hold synkroniseret", diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index b7edc3be..73728519 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -1031,6 +1031,7 @@ "unwatchedOnly": "Nur ungesehene", "nextNUnwatched": "Nächste ${count} ungesehene", "customAmount": "Eigene Anzahl...", + "includeSpecials": "Specials einschließen", "howManyEpisodes": "Wie viele Episoden?", "itemsQueued": "${count} Elemente zum Download eingereiht", "keepSynced": "Synchronisiert halten", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 0529c89a..9c82b6d4 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -1040,6 +1040,7 @@ "unwatchedOnly": "Unwatched only", "nextNUnwatched": "Next ${count} unwatched", "customAmount": "Custom amount...", + "includeSpecials": "Include Specials", "howManyEpisodes": "How many episodes?", "itemsQueued": "${count} items queued for download", "keepSynced": "Keep synced", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index 1192fb39..1d70f5b3 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -1031,6 +1031,7 @@ "unwatchedOnly": "Solo no vistos", "nextNUnwatched": "Próximos ${count} no vistos", "customAmount": "Cantidad personalizada...", + "includeSpecials": "Incluir especiales", "howManyEpisodes": "¿Cuántos episodios?", "itemsQueued": "${count} elementos en cola de descarga", "keepSynced": "Mantener sincronizado", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index 1264f86e..e09af021 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -1031,6 +1031,7 @@ "unwatchedOnly": "Non vus uniquement", "nextNUnwatched": "${count} prochains non vus", "customAmount": "Quantité personnalisée...", + "includeSpecials": "Inclure les spéciaux", "howManyEpisodes": "Combien d'épisodes ?", "itemsQueued": "${count} éléments mis en file d'attente", "keepSynced": "Garder synchronisé", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 34cb9046..22c1d99d 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -1031,6 +1031,7 @@ "unwatchedOnly": "Solo non visti", "nextNUnwatched": "Prossimi ${count} non visti", "customAmount": "Quantità personalizzata...", + "includeSpecials": "Includi gli speciali", "howManyEpisodes": "Quanti episodi?", "itemsQueued": "${count} elementi in coda per il download", "keepSynced": "Mantieni sincronizzato", diff --git a/lib/i18n/ja.i18n.json b/lib/i18n/ja.i18n.json index 9880bdc0..b4b34bc1 100644 --- a/lib/i18n/ja.i18n.json +++ b/lib/i18n/ja.i18n.json @@ -1031,6 +1031,7 @@ "unwatchedOnly": "未視聴のみ", "nextNUnwatched": "次の${count}件の未視聴", "customAmount": "数を指定...", + "includeSpecials": "スペシャルを含める", "howManyEpisodes": "何エピソード?", "itemsQueued": "${count}件をダウンロードキューに追加", "keepSynced": "同期を維持", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 57a2437a..82fb7d23 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -1031,6 +1031,7 @@ "unwatchedOnly": "시청하지 않은 것만", "nextNUnwatched": "다음 ${count}개 미시청", "customAmount": "직접 입력...", + "includeSpecials": "스페셜 포함", "howManyEpisodes": "몇 개의 에피소드?", "itemsQueued": "${count}개 항목이 다운로드 대기열에 추가됨", "keepSynced": "동기화 유지", diff --git a/lib/i18n/nb.i18n.json b/lib/i18n/nb.i18n.json index 87233b6c..774b8099 100644 --- a/lib/i18n/nb.i18n.json +++ b/lib/i18n/nb.i18n.json @@ -1031,6 +1031,7 @@ "unwatchedOnly": "Kun usette", "nextNUnwatched": "Neste ${count} usette", "customAmount": "Egendefinert antall...", + "includeSpecials": "Inkluder spesialepisoder", "howManyEpisodes": "Hvor mange episoder?", "itemsQueued": "${count} elementer i nedlastingskø", "keepSynced": "Hold synkronisert", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index d1a1c11c..ef2c4446 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -1031,6 +1031,7 @@ "unwatchedOnly": "Alleen onbekeken", "nextNUnwatched": "Volgende ${count} onbekeken", "customAmount": "Aangepast aantal...", + "includeSpecials": "Specials opnemen", "howManyEpisodes": "Hoeveel afleveringen?", "itemsQueued": "${count} items in downloadwachtrij", "keepSynced": "Gesynchroniseerd houden", diff --git a/lib/i18n/pl.i18n.json b/lib/i18n/pl.i18n.json index 3a2cd3a3..bd055b63 100644 --- a/lib/i18n/pl.i18n.json +++ b/lib/i18n/pl.i18n.json @@ -1031,6 +1031,7 @@ "unwatchedOnly": "Tylko nieobejrzane", "nextNUnwatched": "Następne ${count} nieobejrzanych", "customAmount": "Własna ilość...", + "includeSpecials": "Uwzględnij odcinki specjalne", "howManyEpisodes": "Ile odcinków?", "itemsQueued": "${count} elementów dodanych do kolejki pobierania", "keepSynced": "Synchronizuj na bieżąco", diff --git a/lib/i18n/pt.i18n.json b/lib/i18n/pt.i18n.json index f45edcfd..c71d932e 100644 --- a/lib/i18n/pt.i18n.json +++ b/lib/i18n/pt.i18n.json @@ -1031,6 +1031,7 @@ "unwatchedOnly": "Apenas não assistidos", "nextNUnwatched": "Próximos ${count} não assistidos", "customAmount": "Quantidade personalizada...", + "includeSpecials": "Incluir especiais", "howManyEpisodes": "Quantos episódios?", "itemsQueued": "${count} itens na fila de download", "keepSynced": "Manter sincronizado", diff --git a/lib/i18n/ru.i18n.json b/lib/i18n/ru.i18n.json index b5f3db73..989b48f0 100644 --- a/lib/i18n/ru.i18n.json +++ b/lib/i18n/ru.i18n.json @@ -1031,6 +1031,7 @@ "unwatchedOnly": "Только непросмотренные", "nextNUnwatched": "Следующие ${count} непросмотренных", "customAmount": "Указать количество...", + "includeSpecials": "Включить спецвыпуски", "howManyEpisodes": "Сколько эпизодов?", "itemsQueued": "${count} элементов добавлено в очередь загрузки", "keepSynced": "Синхронизировать", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 39885018..27abf0a3 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: 20425 (1276 per locale) +/// Strings: 20441 (1277 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 daf87a37..0188d043 100644 --- a/lib/i18n/strings_bg.g.dart +++ b/lib/i18n/strings_bg.g.dart @@ -1288,6 +1288,7 @@ class _TranslationsDownloadsBg extends TranslationsDownloadsEn { @override String get unwatchedOnly => 'Само негледани'; @override String nextNUnwatched({required Object count}) => 'Следващите ${count} негледани'; @override String get customAmount => 'Персонален брой...'; + @override String get includeSpecials => 'Включи специалните'; @override String get howManyEpisodes => 'Колко епизода?'; @override String itemsQueued({required Object count}) => '${count} елемента са добавени в опашката за изтегляне'; @override String get keepSynced => 'Поддържай синхронизирано'; @@ -2878,6 +2879,7 @@ extension on TranslationsBg { 'downloads.unwatchedOnly' => 'Само негледани', 'downloads.nextNUnwatched' => ({required Object count}) => 'Следващите ${count} негледани', 'downloads.customAmount' => 'Персонален брой...', + 'downloads.includeSpecials' => 'Включи специалните', 'downloads.howManyEpisodes' => 'Колко епизода?', 'downloads.itemsQueued' => ({required Object count}) => '${count} елемента са добавени в опашката за изтегляне', 'downloads.keepSynced' => 'Поддържай синхронизирано', @@ -2950,9 +2952,9 @@ extension on TranslationsBg { 'companionRemote.pairing.authFailed' => 'Удостоверяването е неуспешно. Двете устройства трябва да използват същия Plex акаунт.', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Неуспешно свързване: ${error}', 'companionRemote.remote.disconnectConfirm' => 'Искате ли да прекъснете връзката с дистанционната сесия?', - 'companionRemote.remote.reconnecting' => 'Повторно свързване...', _ => null, } ?? switch (path) { + 'companionRemote.remote.reconnecting' => 'Повторно свързване...', 'companionRemote.remote.attemptOf' => ({required Object current}) => 'Опит ${current} от 5', 'companionRemote.remote.retryNow' => 'Опитай сега', 'companionRemote.remote.tabRemote' => 'Дистанционно', diff --git a/lib/i18n/strings_da.g.dart b/lib/i18n/strings_da.g.dart index 06611ed3..eeac4fc1 100644 --- a/lib/i18n/strings_da.g.dart +++ b/lib/i18n/strings_da.g.dart @@ -1288,6 +1288,7 @@ class _TranslationsDownloadsDa extends TranslationsDownloadsEn { @override String get unwatchedOnly => 'Kun usete'; @override String nextNUnwatched({required Object count}) => 'Næste ${count} usete'; @override String get customAmount => 'Angiv antal...'; + @override String get includeSpecials => 'Inkludér specials'; @override String get howManyEpisodes => 'Hvor mange episoder?'; @override String itemsQueued({required Object count}) => '${count} elementer sat i kø til download'; @override String get keepSynced => 'Hold synkroniseret'; @@ -2878,6 +2879,7 @@ extension on TranslationsDa { 'downloads.unwatchedOnly' => 'Kun usete', 'downloads.nextNUnwatched' => ({required Object count}) => 'Næste ${count} usete', 'downloads.customAmount' => 'Angiv antal...', + 'downloads.includeSpecials' => 'Inkludér specials', 'downloads.howManyEpisodes' => 'Hvor mange episoder?', 'downloads.itemsQueued' => ({required Object count}) => '${count} elementer sat i kø til download', 'downloads.keepSynced' => 'Hold synkroniseret', @@ -2950,9 +2952,9 @@ extension on TranslationsDa { 'companionRemote.pairing.authFailed' => 'Godkendelse mislykkedes. Begge enheder skal bruge samme Plex-konto.', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Kunne ikke oprette forbindelse: ${error}', 'companionRemote.remote.disconnectConfirm' => 'Vil du afbryde fra fjernsessionen?', - 'companionRemote.remote.reconnecting' => 'Genopretter forbindelse...', _ => null, } ?? switch (path) { + 'companionRemote.remote.reconnecting' => 'Genopretter forbindelse...', 'companionRemote.remote.attemptOf' => ({required Object current}) => 'Forsøg ${current} af 5', 'companionRemote.remote.retryNow' => 'Prøv igen nu', 'companionRemote.remote.tabRemote' => 'Fjernbetjening', diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index aee18489..333e81a5 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -1288,6 +1288,7 @@ class _TranslationsDownloadsDe extends TranslationsDownloadsEn { @override String get unwatchedOnly => 'Nur ungesehene'; @override String nextNUnwatched({required Object count}) => 'Nächste ${count} ungesehene'; @override String get customAmount => 'Eigene Anzahl...'; + @override String get includeSpecials => 'Specials einschließen'; @override String get howManyEpisodes => 'Wie viele Episoden?'; @override String itemsQueued({required Object count}) => '${count} Elemente zum Download eingereiht'; @override String get keepSynced => 'Synchronisiert halten'; @@ -2878,6 +2879,7 @@ extension on TranslationsDe { 'downloads.unwatchedOnly' => 'Nur ungesehene', 'downloads.nextNUnwatched' => ({required Object count}) => 'Nächste ${count} ungesehene', 'downloads.customAmount' => 'Eigene Anzahl...', + 'downloads.includeSpecials' => 'Specials einschließen', 'downloads.howManyEpisodes' => 'Wie viele Episoden?', 'downloads.itemsQueued' => ({required Object count}) => '${count} Elemente zum Download eingereiht', 'downloads.keepSynced' => 'Synchronisiert halten', @@ -2950,9 +2952,9 @@ extension on TranslationsDe { 'companionRemote.pairing.authFailed' => 'Authentifizierung fehlgeschlagen. Beide Geräte benötigen dasselbe Plex-Konto.', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Verbindung fehlgeschlagen: ${error}', 'companionRemote.remote.disconnectConfirm' => 'Möchtest du die Verbindung zur Fernsteuerungssitzung trennen?', - 'companionRemote.remote.reconnecting' => 'Verbindung wird wiederhergestellt...', _ => null, } ?? switch (path) { + 'companionRemote.remote.reconnecting' => 'Verbindung wird wiederhergestellt...', 'companionRemote.remote.attemptOf' => ({required Object current}) => 'Versuch ${current} von 5', 'companionRemote.remote.retryNow' => 'Jetzt wiederholen', 'companionRemote.remote.tabRemote' => 'Fernbedienung', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index 6257f49c..5a45e782 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -3084,6 +3084,9 @@ class TranslationsDownloadsEn { /// en: 'Custom amount...' String get customAmount => 'Custom amount...'; + /// en: 'Include Specials' + String get includeSpecials => 'Include Specials'; + /// en: 'How many episodes?' String get howManyEpisodes => 'How many episodes?'; @@ -5472,6 +5475,7 @@ extension on Translations { 'downloads.unwatchedOnly' => 'Unwatched only', 'downloads.nextNUnwatched' => ({required Object count}) => 'Next ${count} unwatched', 'downloads.customAmount' => 'Custom amount...', + 'downloads.includeSpecials' => 'Include Specials', 'downloads.howManyEpisodes' => 'How many episodes?', 'downloads.itemsQueued' => ({required Object count}) => '${count} items queued for download', 'downloads.keepSynced' => 'Keep synced', @@ -5535,9 +5539,9 @@ extension on Translations { 'companionRemote.pairing.noDevicesFound' => 'No devices found on your network', 'companionRemote.pairing.noDevicesHint' => 'Open Plezy on desktop and use the same WiFi', 'companionRemote.pairing.availableDevices' => 'Available Devices', - 'companionRemote.pairing.manualConnection' => 'Manual Connection', _ => null, } ?? switch (path) { + 'companionRemote.pairing.manualConnection' => 'Manual Connection', 'companionRemote.pairing.cryptoInitFailed' => 'Couldn\'t start secure connection. Sign in to Plex first.', 'companionRemote.pairing.validationHostRequired' => 'Please enter host address', 'companionRemote.pairing.validationHostFormat' => 'Format must be IP:port (e.g., 192.168.1.100:48632)', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index 81ca6ea2..aa92073c 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -1288,6 +1288,7 @@ class _TranslationsDownloadsEs extends TranslationsDownloadsEn { @override String get unwatchedOnly => 'Solo no vistos'; @override String nextNUnwatched({required Object count}) => 'Próximos ${count} no vistos'; @override String get customAmount => 'Cantidad personalizada...'; + @override String get includeSpecials => 'Incluir especiales'; @override String get howManyEpisodes => '¿Cuántos episodios?'; @override String itemsQueued({required Object count}) => '${count} elementos en cola de descarga'; @override String get keepSynced => 'Mantener sincronizado'; @@ -2878,6 +2879,7 @@ extension on TranslationsEs { 'downloads.unwatchedOnly' => 'Solo no vistos', 'downloads.nextNUnwatched' => ({required Object count}) => 'Próximos ${count} no vistos', 'downloads.customAmount' => 'Cantidad personalizada...', + 'downloads.includeSpecials' => 'Incluir especiales', 'downloads.howManyEpisodes' => '¿Cuántos episodios?', 'downloads.itemsQueued' => ({required Object count}) => '${count} elementos en cola de descarga', 'downloads.keepSynced' => 'Mantener sincronizado', @@ -2950,9 +2952,9 @@ extension on TranslationsEs { 'companionRemote.pairing.authFailed' => 'Autenticación fallida. Ambos dispositivos necesitan la misma cuenta Plex.', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Error al conectar: ${error}', 'companionRemote.remote.disconnectConfirm' => '¿Quieres desconectarte de la sesión remota?', - 'companionRemote.remote.reconnecting' => 'Reconectando...', _ => null, } ?? switch (path) { + 'companionRemote.remote.reconnecting' => 'Reconectando...', 'companionRemote.remote.attemptOf' => ({required Object current}) => 'Intento ${current} de 5', 'companionRemote.remote.retryNow' => 'Reintentar ahora', 'companionRemote.remote.tabRemote' => 'Remoto', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index a72c84a1..92de6494 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -1288,6 +1288,7 @@ class _TranslationsDownloadsFr extends TranslationsDownloadsEn { @override String get unwatchedOnly => 'Non vus uniquement'; @override String nextNUnwatched({required Object count}) => '${count} prochains non vus'; @override String get customAmount => 'Quantité personnalisée...'; + @override String get includeSpecials => 'Inclure les spéciaux'; @override String get howManyEpisodes => 'Combien d\'épisodes ?'; @override String itemsQueued({required Object count}) => '${count} éléments mis en file d\'attente'; @override String get keepSynced => 'Garder synchronisé'; @@ -2878,6 +2879,7 @@ extension on TranslationsFr { 'downloads.unwatchedOnly' => 'Non vus uniquement', 'downloads.nextNUnwatched' => ({required Object count}) => '${count} prochains non vus', 'downloads.customAmount' => 'Quantité personnalisée...', + 'downloads.includeSpecials' => 'Inclure les spéciaux', 'downloads.howManyEpisodes' => 'Combien d\'épisodes ?', 'downloads.itemsQueued' => ({required Object count}) => '${count} éléments mis en file d\'attente', 'downloads.keepSynced' => 'Garder synchronisé', @@ -2950,9 +2952,9 @@ extension on TranslationsFr { 'companionRemote.pairing.authFailed' => 'Échec de l\'authentification. Les deux appareils doivent utiliser le même compte Plex.', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Échec de la connexion : ${error}', 'companionRemote.remote.disconnectConfirm' => 'Voulez-vous vous déconnecter de la session distante ?', - 'companionRemote.remote.reconnecting' => 'Reconnexion...', _ => null, } ?? switch (path) { + 'companionRemote.remote.reconnecting' => 'Reconnexion...', 'companionRemote.remote.attemptOf' => ({required Object current}) => 'Tentative ${current} sur 5', 'companionRemote.remote.retryNow' => 'Réessayer maintenant', 'companionRemote.remote.tabRemote' => 'Télécommande', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index b46a074f..da222c09 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -1288,6 +1288,7 @@ class _TranslationsDownloadsIt extends TranslationsDownloadsEn { @override String get unwatchedOnly => 'Solo non visti'; @override String nextNUnwatched({required Object count}) => 'Prossimi ${count} non visti'; @override String get customAmount => 'Quantità personalizzata...'; + @override String get includeSpecials => 'Includi gli speciali'; @override String get howManyEpisodes => 'Quanti episodi?'; @override String itemsQueued({required Object count}) => '${count} elementi in coda per il download'; @override String get keepSynced => 'Mantieni sincronizzato'; @@ -2878,6 +2879,7 @@ extension on TranslationsIt { 'downloads.unwatchedOnly' => 'Solo non visti', 'downloads.nextNUnwatched' => ({required Object count}) => 'Prossimi ${count} non visti', 'downloads.customAmount' => 'Quantità personalizzata...', + 'downloads.includeSpecials' => 'Includi gli speciali', 'downloads.howManyEpisodes' => 'Quanti episodi?', 'downloads.itemsQueued' => ({required Object count}) => '${count} elementi in coda per il download', 'downloads.keepSynced' => 'Mantieni sincronizzato', @@ -2950,9 +2952,9 @@ extension on TranslationsIt { 'companionRemote.pairing.authFailed' => 'Autenticazione non riuscita. Entrambi i dispositivi devono usare lo stesso account Plex.', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Connessione fallita: ${error}', 'companionRemote.remote.disconnectConfirm' => 'Vuoi disconnetterti dalla sessione remota?', - 'companionRemote.remote.reconnecting' => 'Riconnessione...', _ => null, } ?? switch (path) { + 'companionRemote.remote.reconnecting' => 'Riconnessione...', 'companionRemote.remote.attemptOf' => ({required Object current}) => 'Tentativo ${current} di 5', 'companionRemote.remote.retryNow' => 'Riprova ora', 'companionRemote.remote.tabRemote' => 'Telecomando', diff --git a/lib/i18n/strings_ja.g.dart b/lib/i18n/strings_ja.g.dart index a10203c7..7586d7d6 100644 --- a/lib/i18n/strings_ja.g.dart +++ b/lib/i18n/strings_ja.g.dart @@ -1288,6 +1288,7 @@ class _TranslationsDownloadsJa extends TranslationsDownloadsEn { @override String get unwatchedOnly => '未視聴のみ'; @override String nextNUnwatched({required Object count}) => '次の${count}件の未視聴'; @override String get customAmount => '数を指定...'; + @override String get includeSpecials => 'スペシャルを含める'; @override String get howManyEpisodes => '何エピソード?'; @override String itemsQueued({required Object count}) => '${count}件をダウンロードキューに追加'; @override String get keepSynced => '同期を維持'; @@ -2878,6 +2879,7 @@ extension on TranslationsJa { 'downloads.unwatchedOnly' => '未視聴のみ', 'downloads.nextNUnwatched' => ({required Object count}) => '次の${count}件の未視聴', 'downloads.customAmount' => '数を指定...', + 'downloads.includeSpecials' => 'スペシャルを含める', 'downloads.howManyEpisodes' => '何エピソード?', 'downloads.itemsQueued' => ({required Object count}) => '${count}件をダウンロードキューに追加', 'downloads.keepSynced' => '同期を維持', @@ -2950,9 +2952,9 @@ extension on TranslationsJa { 'companionRemote.pairing.authFailed' => '認証に失敗しました。両方のデバイスで同じPlexアカウントが必要です。', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => '接続に失敗しました: ${error}', 'companionRemote.remote.disconnectConfirm' => 'リモートセッションから切断しますか?', - 'companionRemote.remote.reconnecting' => '再接続中...', _ => null, } ?? switch (path) { + 'companionRemote.remote.reconnecting' => '再接続中...', 'companionRemote.remote.attemptOf' => ({required Object current}) => '試行 ${current}/5', 'companionRemote.remote.retryNow' => '今すぐ再試行', 'companionRemote.remote.tabRemote' => 'リモート', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index f51f8ef6..f0b39c35 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -1288,6 +1288,7 @@ class _TranslationsDownloadsKo extends TranslationsDownloadsEn { @override String get unwatchedOnly => '시청하지 않은 것만'; @override String nextNUnwatched({required Object count}) => '다음 ${count}개 미시청'; @override String get customAmount => '직접 입력...'; + @override String get includeSpecials => '스페셜 포함'; @override String get howManyEpisodes => '몇 개의 에피소드?'; @override String itemsQueued({required Object count}) => '${count}개 항목이 다운로드 대기열에 추가됨'; @override String get keepSynced => '동기화 유지'; @@ -2878,6 +2879,7 @@ extension on TranslationsKo { 'downloads.unwatchedOnly' => '시청하지 않은 것만', 'downloads.nextNUnwatched' => ({required Object count}) => '다음 ${count}개 미시청', 'downloads.customAmount' => '직접 입력...', + 'downloads.includeSpecials' => '스페셜 포함', 'downloads.howManyEpisodes' => '몇 개의 에피소드?', 'downloads.itemsQueued' => ({required Object count}) => '${count}개 항목이 다운로드 대기열에 추가됨', 'downloads.keepSynced' => '동기화 유지', @@ -2950,9 +2952,9 @@ extension on TranslationsKo { 'companionRemote.pairing.authFailed' => '인증에 실패했습니다. 두 기기 모두 같은 Plex 계정이 필요합니다.', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => '연결 실패: ${error}', 'companionRemote.remote.disconnectConfirm' => '원격 세션 연결을 해제하시겠습니까?', - 'companionRemote.remote.reconnecting' => '재연결 중...', _ => null, } ?? switch (path) { + 'companionRemote.remote.reconnecting' => '재연결 중...', 'companionRemote.remote.attemptOf' => ({required Object current}) => '${current}/5 시도 중', 'companionRemote.remote.retryNow' => '지금 재시도', 'companionRemote.remote.tabRemote' => '리모컨', diff --git a/lib/i18n/strings_nb.g.dart b/lib/i18n/strings_nb.g.dart index ae57f3ac..b607daa1 100644 --- a/lib/i18n/strings_nb.g.dart +++ b/lib/i18n/strings_nb.g.dart @@ -1288,6 +1288,7 @@ class _TranslationsDownloadsNb extends TranslationsDownloadsEn { @override String get unwatchedOnly => 'Kun usette'; @override String nextNUnwatched({required Object count}) => 'Neste ${count} usette'; @override String get customAmount => 'Egendefinert antall...'; + @override String get includeSpecials => 'Inkluder spesialepisoder'; @override String get howManyEpisodes => 'Hvor mange episoder?'; @override String itemsQueued({required Object count}) => '${count} elementer i nedlastingskø'; @override String get keepSynced => 'Hold synkronisert'; @@ -2878,6 +2879,7 @@ extension on TranslationsNb { 'downloads.unwatchedOnly' => 'Kun usette', 'downloads.nextNUnwatched' => ({required Object count}) => 'Neste ${count} usette', 'downloads.customAmount' => 'Egendefinert antall...', + 'downloads.includeSpecials' => 'Inkluder spesialepisoder', 'downloads.howManyEpisodes' => 'Hvor mange episoder?', 'downloads.itemsQueued' => ({required Object count}) => '${count} elementer i nedlastingskø', 'downloads.keepSynced' => 'Hold synkronisert', @@ -2950,9 +2952,9 @@ extension on TranslationsNb { 'companionRemote.pairing.authFailed' => 'Autentisering mislyktes. Begge enheter må bruke samme Plex-konto.', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Kunne ikke koble til: ${error}', 'companionRemote.remote.disconnectConfirm' => 'Vil du koble fra fjernøkten?', - 'companionRemote.remote.reconnecting' => 'Kobler til på nytt...', _ => null, } ?? switch (path) { + 'companionRemote.remote.reconnecting' => 'Kobler til på nytt...', 'companionRemote.remote.attemptOf' => ({required Object current}) => 'Forsøk ${current} av 5', 'companionRemote.remote.retryNow' => 'Prøv nå', 'companionRemote.remote.tabRemote' => 'Fjernkontroll', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index 9ccacc18..47bb1eea 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -1288,6 +1288,7 @@ class _TranslationsDownloadsNl extends TranslationsDownloadsEn { @override String get unwatchedOnly => 'Alleen onbekeken'; @override String nextNUnwatched({required Object count}) => 'Volgende ${count} onbekeken'; @override String get customAmount => 'Aangepast aantal...'; + @override String get includeSpecials => 'Specials opnemen'; @override String get howManyEpisodes => 'Hoeveel afleveringen?'; @override String itemsQueued({required Object count}) => '${count} items in downloadwachtrij'; @override String get keepSynced => 'Gesynchroniseerd houden'; @@ -2878,6 +2879,7 @@ extension on TranslationsNl { 'downloads.unwatchedOnly' => 'Alleen onbekeken', 'downloads.nextNUnwatched' => ({required Object count}) => 'Volgende ${count} onbekeken', 'downloads.customAmount' => 'Aangepast aantal...', + 'downloads.includeSpecials' => 'Specials opnemen', 'downloads.howManyEpisodes' => 'Hoeveel afleveringen?', 'downloads.itemsQueued' => ({required Object count}) => '${count} items in downloadwachtrij', 'downloads.keepSynced' => 'Gesynchroniseerd houden', @@ -2950,9 +2952,9 @@ extension on TranslationsNl { 'companionRemote.pairing.authFailed' => 'Authenticatie mislukt. Beide apparaten hebben hetzelfde Plex-account nodig.', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Kan niet verbinden: ${error}', 'companionRemote.remote.disconnectConfirm' => 'Wil je de verbinding met de externe sessie verbreken?', - 'companionRemote.remote.reconnecting' => 'Opnieuw verbinden...', _ => null, } ?? switch (path) { + 'companionRemote.remote.reconnecting' => 'Opnieuw verbinden...', 'companionRemote.remote.attemptOf' => ({required Object current}) => 'Poging ${current} van 5', 'companionRemote.remote.retryNow' => 'Nu opnieuw proberen', 'companionRemote.remote.tabRemote' => 'Afstandsbediening', diff --git a/lib/i18n/strings_pl.g.dart b/lib/i18n/strings_pl.g.dart index 60a7b226..58ad3c99 100644 --- a/lib/i18n/strings_pl.g.dart +++ b/lib/i18n/strings_pl.g.dart @@ -1288,6 +1288,7 @@ class _TranslationsDownloadsPl extends TranslationsDownloadsEn { @override String get unwatchedOnly => 'Tylko nieobejrzane'; @override String nextNUnwatched({required Object count}) => 'Następne ${count} nieobejrzanych'; @override String get customAmount => 'Własna ilość...'; + @override String get includeSpecials => 'Uwzględnij odcinki specjalne'; @override String get howManyEpisodes => 'Ile odcinków?'; @override String itemsQueued({required Object count}) => '${count} elementów dodanych do kolejki pobierania'; @override String get keepSynced => 'Synchronizuj na bieżąco'; @@ -2878,6 +2879,7 @@ extension on TranslationsPl { 'downloads.unwatchedOnly' => 'Tylko nieobejrzane', 'downloads.nextNUnwatched' => ({required Object count}) => 'Następne ${count} nieobejrzanych', 'downloads.customAmount' => 'Własna ilość...', + 'downloads.includeSpecials' => 'Uwzględnij odcinki specjalne', 'downloads.howManyEpisodes' => 'Ile odcinków?', 'downloads.itemsQueued' => ({required Object count}) => '${count} elementów dodanych do kolejki pobierania', 'downloads.keepSynced' => 'Synchronizuj na bieżąco', @@ -2950,9 +2952,9 @@ extension on TranslationsPl { 'companionRemote.pairing.authFailed' => 'Uwierzytelnianie nie powiodło się. Oba urządzenia muszą używać tego samego konta Plex.', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Nie udało się połączyć: ${error}', 'companionRemote.remote.disconnectConfirm' => 'Czy chcesz się rozłączyć od sesji zdalnej?', - 'companionRemote.remote.reconnecting' => 'Ponowne łączenie...', _ => null, } ?? switch (path) { + 'companionRemote.remote.reconnecting' => 'Ponowne łączenie...', 'companionRemote.remote.attemptOf' => ({required Object current}) => 'Próba ${current} z 5', 'companionRemote.remote.retryNow' => 'Ponów teraz', 'companionRemote.remote.tabRemote' => 'Pilot', diff --git a/lib/i18n/strings_pt.g.dart b/lib/i18n/strings_pt.g.dart index 4f4550c8..1e7498e5 100644 --- a/lib/i18n/strings_pt.g.dart +++ b/lib/i18n/strings_pt.g.dart @@ -1288,6 +1288,7 @@ class _TranslationsDownloadsPt extends TranslationsDownloadsEn { @override String get unwatchedOnly => 'Apenas não assistidos'; @override String nextNUnwatched({required Object count}) => 'Próximos ${count} não assistidos'; @override String get customAmount => 'Quantidade personalizada...'; + @override String get includeSpecials => 'Incluir especiais'; @override String get howManyEpisodes => 'Quantos episódios?'; @override String itemsQueued({required Object count}) => '${count} itens na fila de download'; @override String get keepSynced => 'Manter sincronizado'; @@ -2878,6 +2879,7 @@ extension on TranslationsPt { 'downloads.unwatchedOnly' => 'Apenas não assistidos', 'downloads.nextNUnwatched' => ({required Object count}) => 'Próximos ${count} não assistidos', 'downloads.customAmount' => 'Quantidade personalizada...', + 'downloads.includeSpecials' => 'Incluir especiais', 'downloads.howManyEpisodes' => 'Quantos episódios?', 'downloads.itemsQueued' => ({required Object count}) => '${count} itens na fila de download', 'downloads.keepSynced' => 'Manter sincronizado', @@ -2950,9 +2952,9 @@ extension on TranslationsPt { 'companionRemote.pairing.authFailed' => 'Falha na autenticação. Ambos os dispositivos precisam da mesma conta Plex.', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Falha ao conectar: ${error}', 'companionRemote.remote.disconnectConfirm' => 'Deseja desconectar da sessão remota?', - 'companionRemote.remote.reconnecting' => 'Reconectando...', _ => null, } ?? switch (path) { + 'companionRemote.remote.reconnecting' => 'Reconectando...', 'companionRemote.remote.attemptOf' => ({required Object current}) => 'Tentativa ${current} de 5', 'companionRemote.remote.retryNow' => 'Tentar Agora', 'companionRemote.remote.tabRemote' => 'Remoto', diff --git a/lib/i18n/strings_ru.g.dart b/lib/i18n/strings_ru.g.dart index 4ab93129..42e0a4be 100644 --- a/lib/i18n/strings_ru.g.dart +++ b/lib/i18n/strings_ru.g.dart @@ -1288,6 +1288,7 @@ class _TranslationsDownloadsRu extends TranslationsDownloadsEn { @override String get unwatchedOnly => 'Только непросмотренные'; @override String nextNUnwatched({required Object count}) => 'Следующие ${count} непросмотренных'; @override String get customAmount => 'Указать количество...'; + @override String get includeSpecials => 'Включить спецвыпуски'; @override String get howManyEpisodes => 'Сколько эпизодов?'; @override String itemsQueued({required Object count}) => '${count} элементов добавлено в очередь загрузки'; @override String get keepSynced => 'Синхронизировать'; @@ -2878,6 +2879,7 @@ extension on TranslationsRu { 'downloads.unwatchedOnly' => 'Только непросмотренные', 'downloads.nextNUnwatched' => ({required Object count}) => 'Следующие ${count} непросмотренных', 'downloads.customAmount' => 'Указать количество...', + 'downloads.includeSpecials' => 'Включить спецвыпуски', 'downloads.howManyEpisodes' => 'Сколько эпизодов?', 'downloads.itemsQueued' => ({required Object count}) => '${count} элементов добавлено в очередь загрузки', 'downloads.keepSynced' => 'Синхронизировать', @@ -2950,9 +2952,9 @@ extension on TranslationsRu { 'companionRemote.pairing.authFailed' => 'Аутентификация не удалась. На обоих устройствах нужен один аккаунт Plex.', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Не удалось подключиться: ${error}', 'companionRemote.remote.disconnectConfirm' => 'Отключиться от удалённой сессии?', - 'companionRemote.remote.reconnecting' => 'Переподключение...', _ => null, } ?? switch (path) { + 'companionRemote.remote.reconnecting' => 'Переподключение...', 'companionRemote.remote.attemptOf' => ({required Object current}) => 'Попытка ${current} из 5', 'companionRemote.remote.retryNow' => 'Повторить сейчас', 'companionRemote.remote.tabRemote' => 'Пульт', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index 2ec771c0..57d50dd7 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -1288,6 +1288,7 @@ class _TranslationsDownloadsSv extends TranslationsDownloadsEn { @override String get unwatchedOnly => 'Endast osedda'; @override String nextNUnwatched({required Object count}) => 'Nästa ${count} osedda'; @override String get customAmount => 'Ange antal...'; + @override String get includeSpecials => 'Inkludera specialavsnitt'; @override String get howManyEpisodes => 'Hur många avsnitt?'; @override String itemsQueued({required Object count}) => '${count} objekt köade för nedladdning'; @override String get keepSynced => 'Håll synkroniserad'; @@ -2878,6 +2879,7 @@ extension on TranslationsSv { 'downloads.unwatchedOnly' => 'Endast osedda', 'downloads.nextNUnwatched' => ({required Object count}) => 'Nästa ${count} osedda', 'downloads.customAmount' => 'Ange antal...', + 'downloads.includeSpecials' => 'Inkludera specialavsnitt', 'downloads.howManyEpisodes' => 'Hur många avsnitt?', 'downloads.itemsQueued' => ({required Object count}) => '${count} objekt köade för nedladdning', 'downloads.keepSynced' => 'Håll synkroniserad', @@ -2950,9 +2952,9 @@ extension on TranslationsSv { 'companionRemote.pairing.authFailed' => 'Autentisering misslyckades. Båda enheter behöver samma Plex-konto.', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Kunde inte ansluta: ${error}', 'companionRemote.remote.disconnectConfirm' => 'Vill du koppla från fjärrsessionen?', - 'companionRemote.remote.reconnecting' => 'Återansluter...', _ => null, } ?? switch (path) { + 'companionRemote.remote.reconnecting' => 'Återansluter...', 'companionRemote.remote.attemptOf' => ({required Object current}) => 'Försök ${current} av 5', 'companionRemote.remote.retryNow' => 'Försök nu', 'companionRemote.remote.tabRemote' => 'Fjärrkontroll', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index 7c0fc3f5..f91a4ff5 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -1288,6 +1288,7 @@ class _TranslationsDownloadsZh extends TranslationsDownloadsEn { @override String get unwatchedOnly => '仅未观看'; @override String nextNUnwatched({required Object count}) => '接下来 ${count} 集未观看'; @override String get customAmount => '自定义数量...'; + @override String get includeSpecials => '包含特别篇'; @override String get howManyEpisodes => '下载几集?'; @override String itemsQueued({required Object count}) => '${count} 个项目已加入下载队列'; @override String get keepSynced => '保持同步'; @@ -2878,6 +2879,7 @@ extension on TranslationsZh { 'downloads.unwatchedOnly' => '仅未观看', 'downloads.nextNUnwatched' => ({required Object count}) => '接下来 ${count} 集未观看', 'downloads.customAmount' => '自定义数量...', + 'downloads.includeSpecials' => '包含特别篇', 'downloads.howManyEpisodes' => '下载几集?', 'downloads.itemsQueued' => ({required Object count}) => '${count} 个项目已加入下载队列', 'downloads.keepSynced' => '保持同步', @@ -2950,9 +2952,9 @@ extension on TranslationsZh { 'companionRemote.pairing.authFailed' => '认证失败。两台设备需要使用同一 Plex 账号。', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => '连接失败:${error}', 'companionRemote.remote.disconnectConfirm' => '是否要断开远程会话的连接?', - 'companionRemote.remote.reconnecting' => '重新连接中...', _ => null, } ?? switch (path) { + 'companionRemote.remote.reconnecting' => '重新连接中...', 'companionRemote.remote.attemptOf' => ({required Object current}) => '第 ${current} 次尝试,共 5 次', 'companionRemote.remote.retryNow' => '立即重试', 'companionRemote.remote.tabRemote' => '遥控', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index ac63b1f0..e3b4c080 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -1031,6 +1031,7 @@ "unwatchedOnly": "Endast osedda", "nextNUnwatched": "Nästa ${count} osedda", "customAmount": "Ange antal...", + "includeSpecials": "Inkludera specialavsnitt", "howManyEpisodes": "Hur många avsnitt?", "itemsQueued": "${count} objekt köade för nedladdning", "keepSynced": "Håll synkroniserad", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index 6ccede69..3707f0e7 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -1031,6 +1031,7 @@ "unwatchedOnly": "仅未观看", "nextNUnwatched": "接下来 ${count} 集未观看", "customAmount": "自定义数量...", + "includeSpecials": "包含特别篇", "howManyEpisodes": "下载几集?", "itemsQueued": "${count} 个项目已加入下载队列", "keepSynced": "保持同步", diff --git a/lib/media/episode_collection.dart b/lib/media/episode_collection.dart index 3aff89fc..f9fe1700 100644 --- a/lib/media/episode_collection.dart +++ b/lib/media/episode_collection.dart @@ -20,8 +20,16 @@ Future collectEpisodesForShow( required bool unwatchedOnly, required List out, MediaItem? fallback, + bool includeSpecials = true, }) { - return _collectPlayable(client, showRatingKey, unwatchedOnly: unwatchedOnly, out: out, fallback: fallback); + return _collectPlayable( + client, + showRatingKey, + unwatchedOnly: unwatchedOnly, + out: out, + fallback: fallback, + includeSpecials: includeSpecials, + ); } /// Collect every episode of a single season into [out] via the same @@ -33,8 +41,16 @@ Future collectEpisodesForSeason( required bool unwatchedOnly, required List out, MediaItem? fallback, + bool includeSpecials = true, }) { - return _collectPlayable(client, seasonRatingKey, unwatchedOnly: unwatchedOnly, out: out, fallback: fallback); + return _collectPlayable( + client, + seasonRatingKey, + unwatchedOnly: unwatchedOnly, + out: out, + fallback: fallback, + includeSpecials: includeSpecials, + ); } /// Fetch just the first episode of a season without walking the entire season. @@ -253,6 +269,7 @@ Future _collectPlayable( required bool unwatchedOnly, required List out, MediaItem? fallback, + bool includeSpecials = true, }) async { final leaves = await client.fetchPlayableDescendants(parentId); // Collect into a local list and order it before handing back: the backend @@ -266,6 +283,7 @@ Future _collectPlayable( final collected = []; for (final ep in leaves) { if (ep.kind != MediaKind.episode) continue; + if (!includeSpecials && isSpecialSeasonNumber(ep.parentIndex)) continue; if (unwatchedOnly && !ep.isUnwatchedOrInProgress) continue; collected.add(_withFallbackLibrary(ep, fallback)); } diff --git a/lib/providers/download_provider.dart b/lib/providers/download_provider.dart index 8f19d8a1..70b91e95 100644 --- a/lib/providers/download_provider.dart +++ b/lib/providers/download_provider.dart @@ -845,6 +845,7 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin DownloadVersionConfig? versionConfig, DownloadFilter filter = DownloadFilter.all, int? maxCount, + bool includeSpecials = true, }) async { if (!_downloadManager.downloadsSupported) return 0; @@ -870,7 +871,14 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin final hadMetadata = _metadata.containsKey(globalKey); _metadata[globalKey] = metadata; try { - return await _queueShowDownload(metadata, client, versionConfig: config, filter: filter, maxCount: maxCount); + return await _queueShowDownload( + metadata, + client, + versionConfig: config, + filter: filter, + maxCount: maxCount, + includeSpecials: includeSpecials, + ); } catch (_) { if (!hadMetadata) _metadata.remove(globalKey); rethrow; @@ -885,6 +893,7 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin versionConfig: config, filter: filter, maxCount: maxCount, + includeSpecials: includeSpecials, ); } catch (_) { if (!hadMetadata) _metadata.remove(globalKey); @@ -1120,6 +1129,7 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin DownloadVersionConfig? versionConfig, DownloadFilter filter = DownloadFilter.all, int? maxCount, + bool includeSpecials = true, }) async { return _expandAndQueue( container: show, @@ -1128,6 +1138,7 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin filter: filter, maxCount: maxCount, skipExisting: false, + includeSpecials: includeSpecials, ); } @@ -1138,6 +1149,7 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin DownloadVersionConfig? versionConfig, DownloadFilter filter = DownloadFilter.all, int? maxCount, + bool includeSpecials = true, }) async { return _expandAndQueue( container: season, @@ -1146,6 +1158,7 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin filter: filter, maxCount: maxCount, skipExisting: false, + includeSpecials: includeSpecials, ); } @@ -1183,8 +1196,13 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin required DownloadFilter filter, required int? maxCount, required bool skipExisting, + bool includeSpecials = true, }) async { final unwatchedOnly = filter == DownloadFilter.unwatched; + // Downloading the Specials season itself must still queue its episodes — + // only suppress Specials when sweeping a whole show or a regular season. + final effectiveIncludeSpecials = + includeSpecials || (container.kind == MediaKind.season && isSpecialSeasonNumber(container.index)); final relatedContext = _RelatedMetadataDownloadContext(); final episodes = []; if (container.kind == MediaKind.show) { @@ -1194,6 +1212,7 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin unwatchedOnly: unwatchedOnly, out: episodes, fallback: container, + includeSpecials: effectiveIncludeSpecials, ); } else { await collectEpisodesForSeason( @@ -1202,6 +1221,7 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin unwatchedOnly: unwatchedOnly, out: episodes, fallback: container, + includeSpecials: effectiveIncludeSpecials, ); } @@ -1552,6 +1572,7 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin required int episodeCount, int mediaIndex = 0, String downloadFilter = SyncRuleFilter.unwatched, + bool includeSpecials = true, MediaItem? targetMetadata, }) async { final profileId = _requireActiveProfileId(); @@ -1566,6 +1587,7 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin episodeCount: episodeCount, mediaIndex: mediaIndex, downloadFilter: downloadFilter, + includeSpecials: includeSpecials, ); if (targetMetadata != null) { @@ -1579,7 +1601,10 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin _syncRules[rule.globalKey] = rule; safeNotifyListeners(); } - appLogger.i('Created sync rule: $scopedGlobalKey ($targetType, filter=$downloadFilter, keep $episodeCount)'); + appLogger.i( + 'Created sync rule: $scopedGlobalKey ' + '($targetType, filter=$downloadFilter, keep $episodeCount, includeSpecials=$includeSpecials)', + ); } /// Update the episode count for an existing show/season sync rule. diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 71d14912..f1f58999 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -383,6 +383,11 @@ class SettingsService extends BaseSharedPreferencesService { static const customDownloadPathType = NullableStringPref('custom_download_path_type'); static const downloadOnWifiOnly = BoolPref('download_on_wifi_only'); static const autoRemoveWatchedDownloads = BoolPref('auto_remove_watched_downloads'); + + /// Remembered state of the "Include Specials" toggle on the show download + /// dialog. Defaults to true (include) so existing behavior is unchanged; + /// turning it off persists so the next download keeps the choice. + static const downloadIncludeSpecials = BoolPref('download_include_specials', defaultValue: true); static const autoCheckUpdatesOnStartup = BoolPref('auto_check_updates_on_startup', defaultValue: true); static const showPerformanceOverlay = BoolPref('show_performance_overlay'); static const autoHidePerformanceOverlay = BoolPref('auto_hide_performance_overlay', defaultValue: true); @@ -798,6 +803,7 @@ class SettingsService extends BaseSharedPreferencesService { rememberTrackSelections, customDownloadPathType, downloadOnWifiOnly, + downloadIncludeSpecials, autoCheckUpdatesOnStartup, showPerformanceOverlay, autoHidePerformanceOverlay, diff --git a/lib/services/sync_rule_executor.dart b/lib/services/sync_rule_executor.dart index 4d3927ac..47f9373b 100644 --- a/lib/services/sync_rule_executor.dart +++ b/lib/services/sync_rule_executor.dart @@ -261,6 +261,7 @@ class SyncRuleExecutor { unwatchedOnly: true, out: fromServer, fallback: sourceMetadata, + includeSpecials: rule.includeSpecials, ); } else { await collectEpisodesForSeason( diff --git a/lib/utils/dialogs.dart b/lib/utils/dialogs.dart index 9d09aba4..cbf25d2a 100644 --- a/lib/utils/dialogs.dart +++ b/lib/utils/dialogs.dart @@ -322,11 +322,17 @@ class _TextInputDialogState extends State<_TextInputDialog> /// Returns the selected value, or null if cancelled. Each option's [icon] may /// be `null` to render a label-only row (useful when the choices are variants /// of the same thing and a repeated icon would just be noise). +/// Optional persistent toggle rendered above the option rows. Its state is held +/// by the dialog (toggling does not pop), and [onChanged] mirrors the new value +/// out so the caller can read it once an option row is picked. +typedef OptionPickerToggle = ({String label, IconData? icon, bool value, ValueChanged onChanged}); + Future showOptionPickerDialog( BuildContext context, { required String title, required List<({IconData? icon, String label, T value})> options, Future Function(T value)? onBeforeClose, + OptionPickerToggle? toggle, }) { final focusFirstItem = InputModeTracker.isKeyboardMode(context); return showScopedDialog( @@ -336,6 +342,7 @@ Future showOptionPickerDialog( options: options, focusFirstItem: focusFirstItem, onBeforeClose: onBeforeClose, + toggle: toggle, ), ); } @@ -345,12 +352,14 @@ class _OptionPickerDialog extends StatefulWidget { final List<({IconData? icon, String label, T value})> options; final bool focusFirstItem; final Future Function(T value)? onBeforeClose; + final OptionPickerToggle? toggle; const _OptionPickerDialog({ required this.title, required this.options, this.focusFirstItem = false, this.onBeforeClose, + this.toggle, }); @override @@ -359,11 +368,13 @@ class _OptionPickerDialog extends StatefulWidget { class _OptionPickerDialogState extends State<_OptionPickerDialog> { late final FocusNode _initialFocusNode; + late bool _toggleValue; @override void initState() { super.initState(); _initialFocusNode = FocusNode(debugLabel: 'OptionPickerInitialFocus'); + _toggleValue = widget.toggle?.value ?? false; if (widget.focusFirstItem) { FocusUtils.requestFocusAfterBuild(this, _initialFocusNode); } @@ -377,27 +388,42 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> { @override Widget build(BuildContext context) { + const rowPadding = EdgeInsets.symmetric(horizontal: 24, vertical: 4); + final toggle = widget.toggle; return SimpleDialog( title: Text(widget.title), contentPadding: const EdgeInsets.symmetric(vertical: 8), - children: List.generate(widget.options.length, (index) { - final option = widget.options[index]; - final icon = option.icon; - return FocusableListTile( - focusNode: index == 0 && widget.focusFirstItem ? _initialFocusNode : null, - leading: icon != null ? AppIcon(icon, fill: 1, size: 24) : null, - title: Text(option.label, style: Theme.of(context).textTheme.bodyLarge), - contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), - onTap: () async { - if (widget.onBeforeClose != null) { - final result = await widget.onBeforeClose!(option.value); - if (context.mounted) Navigator.pop(context, result); - } else { - Navigator.pop(context, option.value); - } - }, - ); - }), + children: [ + if (toggle != null) + FocusableSwitchListTile( + value: _toggleValue, + secondary: toggle.icon != null ? AppIcon(toggle.icon!, fill: 1, size: 24) : null, + title: Text(toggle.label, style: Theme.of(context).textTheme.bodyLarge), + contentPadding: rowPadding, + onChanged: (value) { + setState(() => _toggleValue = value); + toggle.onChanged(value); + }, + ), + ...List.generate(widget.options.length, (index) { + final option = widget.options[index]; + final icon = option.icon; + return FocusableListTile( + focusNode: index == 0 && widget.focusFirstItem ? _initialFocusNode : null, + leading: icon != null ? AppIcon(icon, fill: 1, size: 24) : null, + title: Text(option.label, style: Theme.of(context).textTheme.bodyLarge), + contentPadding: rowPadding, + onTap: () async { + if (widget.onBeforeClose != null) { + final result = await widget.onBeforeClose!(option.value); + if (context.mounted) Navigator.pop(context, result); + } else { + Navigator.pop(context, option.value); + } + }, + ); + }), + ], ); } } diff --git a/lib/utils/download_utils.dart b/lib/utils/download_utils.dart index 9e79c565..755581ed 100644 --- a/lib/utils/download_utils.dart +++ b/lib/utils/download_utils.dart @@ -8,6 +8,7 @@ import '../media/media_kind.dart'; import '../media/media_server_client.dart'; import '../database/app_database.dart'; import '../providers/download_provider.dart'; +import '../services/settings_service.dart'; import '../services/sync_rule_executor.dart'; import 'content_utils.dart'; import 'dialogs.dart'; @@ -67,6 +68,10 @@ Future showDownloadOptionsAndQueue( var filter = DownloadFilter.all; int? maxCount; bool keepSynced = false; + // Remembered "Include Specials" choice; the toggle is only shown for whole + // shows (a single season has no Specials to drop). + final settings = SettingsService.instanceOrNull; + bool includeSpecials = settings?.read(SettingsService.downloadIncludeSpecials) ?? true; if (kind == MediaKind.show || kind == MediaKind.season) { int? customCount; @@ -89,6 +94,14 @@ Future showDownloadOptionsAndQueue( context, title: t.downloads.downloadNow, options: options, + toggle: kind == MediaKind.show + ? ( + label: t.downloads.includeSpecials, + icon: Symbols.star_rounded, + value: includeSpecials, + onChanged: (value) => includeSpecials = value, + ) + : null, onBeforeClose: (value) async { if (value != _DownloadChoice.custom) return value; customCount = await _showEpisodeCountDialog(context); @@ -149,16 +162,23 @@ Future showDownloadOptionsAndQueue( targetType: metadata.kind.id.isNotEmpty ? metadata.kind.id : ContentTypes.show, episodeCount: syncCount, mediaIndex: versionConfig.mediaIndex, + includeSpecials: includeSpecials, targetMetadata: metadata, ); } + // Remember the toggle for next time (only shown, and thus meaningful, for shows). + if (kind == MediaKind.show) { + await settings?.write(SettingsService.downloadIncludeSpecials, includeSpecials); + } + final count = await downloadProvider.queueDownload( metadata, client, versionConfig: versionConfig, filter: filter, maxCount: maxCount, + includeSpecials: includeSpecials, ); return DownloadResult( diff --git a/lib/widgets/focusable_list_tile.dart b/lib/widgets/focusable_list_tile.dart index e10b77eb..8dfd742f 100644 --- a/lib/widgets/focusable_list_tile.dart +++ b/lib/widgets/focusable_list_tile.dart @@ -273,6 +273,10 @@ class FocusableSwitchListTile extends StatefulWidget { /// Visual density for the list tile. final VisualDensity? visualDensity; + /// Content padding, e.g. to align with sibling rows. Null uses the + /// SwitchListTile default. + final EdgeInsetsGeometry? contentPadding; + const FocusableSwitchListTile({ super.key, this.title, @@ -284,6 +288,7 @@ class FocusableSwitchListTile extends StatefulWidget { this.focusNode, this.autofocus = false, this.visualDensity = const VisualDensity(vertical: -3), + this.contentPadding, }); @override @@ -325,6 +330,7 @@ class _FocusableSwitchListTileState extends State onChanged: widget.onChanged, dense: widget.dense, visualDensity: widget.visualDensity, + contentPadding: widget.contentPadding, focusNode: effectiveFocusNode, autofocus: widget.autofocus, ), diff --git a/test/database/app_database_test.dart b/test/database/app_database_test.dart index f63d48cc..6616ea2d 100644 --- a/test/database/app_database_test.dart +++ b/test/database/app_database_test.dart @@ -40,8 +40,8 @@ class _AppDatabaseTestSuite { // ============================================================ group('schema', () { - test('schemaVersion is 15', () { - expect(db.schemaVersion, 15); + test('schemaVersion is 16', () { + expect(db.schemaVersion, 16); }); test('all tables are accessible and start empty', () async { @@ -865,6 +865,7 @@ class _AppDatabaseTestSuite { expect(rules.first.enabled, isTrue); // default expect(rules.first.downloadFilter, 'unwatched'); // default expect(rules.first.mediaIndex, 0); // default + expect(rules.first.includeSpecials, isTrue); // default expect(rules.first.lastExecutedAt, isNull); }); @@ -888,6 +889,7 @@ class _AppDatabaseTestSuite { targetType: 'season', episodeCount: 99, downloadFilter: 'all', + includeSpecials: false, ); final rules = await db.getSyncRules(); @@ -895,6 +897,7 @@ class _AppDatabaseTestSuite { expect(rules.first.targetType, 'season'); expect(rules.first.episodeCount, 99); expect(rules.first.downloadFilter, 'all'); + expect(rules.first.includeSpecials, isFalse); }); test('insertSyncRule allows the same server item for different profiles', () async { diff --git a/test/providers/download_provider_test.dart b/test/providers/download_provider_test.dart index 11086408..f036c743 100644 --- a/test/providers/download_provider_test.dart +++ b/test/providers/download_provider_test.dart @@ -167,7 +167,13 @@ void main() { var notified = 0; p.addListener(() => notified++); - await p.createSyncRule(serverId: ServerId('srv'), ratingKey: '10', targetType: 'show', episodeCount: 5); + await p.createSyncRule( + serverId: ServerId('srv'), + ratingKey: '10', + targetType: 'show', + episodeCount: 5, + includeSpecials: false, + ); final ruleKey = p.syncRuleKeyFor(ServerId('srv'), '10'); expect(p.hasSyncRule(ruleKey), isTrue); @@ -178,10 +184,12 @@ void main() { expect(rule.episodeCount, 5); expect(rule.enabled, isTrue); expect(rule.downloadFilter, 'unwatched'); // default + expect(rule.includeSpecials, isFalse); // Database state matches in-memory state. final dbRule = await db.getSyncRule(ruleKey); expect(dbRule, isNotNull); expect(dbRule!.targetType, 'show'); + expect(dbRule.includeSpecials, isFalse); // createSyncRule notifies once on success. expect(notified, 1); diff --git a/test/services/sync_rule_executor_test.dart b/test/services/sync_rule_executor_test.dart index 0be55edf..53cee27d 100644 --- a/test/services/sync_rule_executor_test.dart +++ b/test/services/sync_rule_executor_test.dart @@ -299,6 +299,53 @@ void main() { expect(paths.where((p) => p.startsWith('GET /Items?')), isNotEmpty); }); + test('show sync rule respects includeSpecials=false when expanding episodes', () async { + final db = AppDatabase.forTesting(NativeDatabase.memory()); + final manager = MultiServerManager(); + addTearDown(() async { + manager.dispose(); + await db.close(); + }); + + final client = _PlayableDescendantsClient([ + _episode('s1e1', parentIndex: 1, index: 1, originallyAvailableAt: '2022-10-05'), + _episode('s0e1', parentIndex: 0, index: 1, originallyAvailableAt: '2022-10-27'), + _episode('s1e2', parentIndex: 1, index: 2, originallyAvailableAt: '2022-11-02'), + ]); + manager.debugRegisterClientForTesting(client); + + const ruleKey = 'profile-a|plex-machine:show-1'; + final show = MediaItem(id: 'show-1', backend: MediaBackend.plex, kind: MediaKind.show, title: 'Show'); + await db.insertSyncRule( + profileId: 'profile-a', + serverId: ServerId('plex-machine'), + ratingKey: 'show-1', + globalKey: ruleKey, + targetType: 'show', + episodeCount: 0, + includeSpecials: false, + ); + + final queued = []; + final executor = SyncRuleExecutor(database: db); + final results = await executor.executeSyncRules( + profileId: 'profile-a', + serverManager: manager, + downloads: const {}, + metadata: {ruleKey: show}, + queueSingleDownload: (item, client, {int mediaIndex = 0}) async { + queued.add(item); + return true; + }, + isOffline: false, + force: true, + ); + + expect(results.single.queuedCount, 2); + expect(queued.map((item) => item.id), ['s1e1', 's1e2']); + expect(client.fetchPlayableDescendantsCalls, ['show-1']); + }); + test('collection sync rule pages through collection API instead of metadata children', () async { final db = AppDatabase.forTesting(NativeDatabase.memory()); final manager = MultiServerManager(); @@ -351,6 +398,55 @@ void main() { }); } +MediaItem _episode(String id, {required int parentIndex, required int index, String? originallyAvailableAt}) { + return MediaItem( + id: id, + backend: MediaBackend.plex, + kind: MediaKind.episode, + title: id, + parentIndex: parentIndex, + index: index, + originallyAvailableAt: originallyAvailableAt, + ); +} + +class _PlayableDescendantsClient implements MediaServerClient { + _PlayableDescendantsClient(this.leaves); + + final List leaves; + final fetchPlayableDescendantsCalls = []; + + @override + ServerId get serverId => ServerId('plex-machine'); + + @override + String? get serverName => 'Plex'; + + @override + MediaBackend get backend => MediaBackend.plex; + + @override + ServerCapabilities get capabilities => ServerCapabilities.plex; + + @override + bool get isOfflineMode => false; + + @override + void close() {} + + @override + Future fetchItem(String id) async => null; + + @override + Future> fetchPlayableDescendants(String parentId) async { + fetchPlayableDescendantsCalls.add(parentId); + return leaves; + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + class _CollectionPagingClient implements MediaServerClient { bool fetchChildrenCalled = false; final collectionPageCalls = <({int? start, int? size})>[]; diff --git a/test/utils/episode_collection_test.dart b/test/utils/episode_collection_test.dart index af62d3d8..1b02979d 100644 --- a/test/utils/episode_collection_test.dart +++ b/test/utils/episode_collection_test.dart @@ -104,7 +104,36 @@ class _SeasonPagingRecordingClient extends _RecordingClient implements SeasonEpi } } +class _LeavesClient implements MediaServerClient { + _LeavesClient(this.leaves); + + final List leaves; + + @override + Future> fetchPlayableDescendants(String parentId) async => leaves; + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + void main() { + test('collectEpisodesForShow drops Specials when includeSpecials is false', () async { + final client = _LeavesClient([ + _episode('s1e1', parentIndex: 1, index: 1, originallyAvailableAt: '2022-10-05'), + _episode('s0e1', parentIndex: 0, index: 1, originallyAvailableAt: '2022-10-27'), + _episode('s1e2', parentIndex: 1, index: 2, originallyAvailableAt: '2022-11-02'), + ]); + + final withoutSpecials = []; + await collectEpisodesForShow(client, 'show-1', unwatchedOnly: false, out: withoutSpecials, includeSpecials: false); + expect(withoutSpecials.map((e) => e.id), ['s1e1', 's1e2']); + + // Default keeps Specials, interleaved into aired order. + final withSpecials = []; + await collectEpisodesForShow(client, 'show-1', unwatchedOnly: false, out: withSpecials); + expect(withSpecials.map((e) => e.id), ['s1e1', 's0e1', 's1e2']); + }); + test('defaultPlaybackSeason skips specials when a regular season exists', () { final special = _season('specials', index: 0); final season1 = _season('season-1');