refactor: share search field, auth dialog, and list-download plumbing

The search screens, the out-of-band auth dialogs, the live TV guide and the
list-download paths each carried their own copy of the same shell. Extracts
SearchInputField and PendingAuthDialog and routes the duplicated download
and guide helpers through one implementation.
This commit is contained in:
edde746
2026-07-26 06:09:49 +02:00
parent 4eaf4423a1
commit 9429a76acc
22 changed files with 576 additions and 701 deletions
+17 -52
View File
@@ -148,14 +148,7 @@ Future<DownloadResult?> showDownloadOptionsAndQueue(
}
if (filter == DownloadFilter.unwatched && kind == MediaKind.show && context.mounted) {
final syncChoice = await showOptionPickerDialog<_SyncChoice>(
context,
title: t.downloads.downloadNow,
options: [
(icon: Symbols.download_rounded, label: t.downloads.downloadOnce, value: _SyncChoice.downloadOnce),
(icon: Symbols.sync_rounded, label: t.downloads.keepSynced, value: _SyncChoice.keepSynced),
],
);
final syncChoice = await _showSyncChoiceDialog(context);
if (syncChoice == null || !context.mounted) return null;
keepSynced = syncChoice == _SyncChoice.keepSynced;
}
@@ -226,22 +219,12 @@ Future<DownloadResult?> showListDownloadOptionsAndQueue(
final selectedFilter = await showOptionPickerDialog<DownloadFilter>(
context,
title: t.downloads.downloadNow,
options: [
(icon: Symbols.download_rounded, label: t.downloads.allEpisodes, value: DownloadFilter.all),
(icon: Symbols.visibility_off_rounded, label: t.downloads.unwatchedOnly, value: DownloadFilter.unwatched),
],
options: _filterOptions(DownloadFilter.all, DownloadFilter.unwatched),
);
if (selectedFilter == null || !context.mounted) return null;
final syncChoice = await showOptionPickerDialog<_SyncChoice>(
context,
title: t.downloads.downloadNow,
options: [
(icon: Symbols.download_rounded, label: t.downloads.downloadOnce, value: _SyncChoice.downloadOnce),
(icon: Symbols.sync_rounded, label: t.downloads.keepSynced, value: _SyncChoice.keepSynced),
],
);
final syncChoice = await _showSyncChoiceDialog(context);
if (syncChoice == null || !context.mounted) return null;
final serverId = rootMetadata.serverId ?? client.serverId;
@@ -279,36 +262,21 @@ Future<DownloadResult?> showListDownloadOptionsAndQueue(
);
}
/// Shows the shared list-download dialog for a playlist.
Future<DownloadResult?> showPlaylistDownloadOptionsAndQueue(
BuildContext context, {
required MediaItem playlistMetadata,
required List<MediaItem> items,
required MediaServerClient client,
required DownloadProvider downloadProvider,
}) => showListDownloadOptionsAndQueue(
context,
rootMetadata: playlistMetadata,
targetType: ContentTypes.playlist,
items: items,
client: client,
downloadProvider: downloadProvider,
);
/// The all/unwatched option rows, shared by the pickers that differ only in
/// how they spell those two values.
List<({IconData? icon, String label, T value})> _filterOptions<T>(T all, T unwatched) => [
(icon: Symbols.download_rounded, label: t.downloads.allEpisodes, value: all),
(icon: Symbols.visibility_off_rounded, label: t.downloads.unwatchedOnly, value: unwatched),
];
/// Shows the shared list-download dialog for a collection.
Future<DownloadResult?> showCollectionDownloadOptionsAndQueue(
BuildContext context, {
required MediaItem collectionMetadata,
required List<MediaItem> items,
required MediaServerClient client,
required DownloadProvider downloadProvider,
}) => showListDownloadOptionsAndQueue(
/// Asks whether to download once or keep the target synced.
Future<_SyncChoice?> _showSyncChoiceDialog(BuildContext context) => showOptionPickerDialog<_SyncChoice>(
context,
rootMetadata: collectionMetadata,
targetType: ContentTypes.collection,
items: items,
client: client,
downloadProvider: downloadProvider,
title: t.downloads.downloadNow,
options: [
(icon: Symbols.download_rounded, label: t.downloads.downloadOnce, value: _SyncChoice.downloadOnce),
(icon: Symbols.sync_rounded, label: t.downloads.keepSynced, value: _SyncChoice.keepSynced),
],
);
Future<int?> _showEpisodeCountDialog(
@@ -375,10 +343,7 @@ Future<bool> editSyncRuleFilter(
final selected = await showOptionPickerDialog<String>(
context,
title: t.downloads.editSyncFilter,
options: [
(icon: Symbols.download_rounded, label: t.downloads.allEpisodes, value: SyncRuleFilter.all),
(icon: Symbols.visibility_off_rounded, label: t.downloads.unwatchedOnly, value: SyncRuleFilter.unwatched),
],
options: _filterOptions(SyncRuleFilter.all, SyncRuleFilter.unwatched),
);
if (selected == null || selected == currentFilter || !context.mounted) return false;
+7 -12
View File
@@ -45,15 +45,15 @@ List<LiveTvChannelGroup> groupLiveTvChannelsBySource(List<LiveTvChannel> channel
}
String liveTvChannelSourceKey(LiveTvChannel channel) {
final serverId = _nonEmpty(channel.serverId) ?? '';
final providerSource = _nonEmpty(channel.favoriteSource) ?? _nonEmpty(channel.lineup) ?? '';
final dvrSource = _nonEmpty(channel.liveDvrKey) ?? '';
final serverId = liveTvNonEmpty(channel.serverId) ?? '';
final providerSource = liveTvNonEmpty(channel.favoriteSource) ?? liveTvNonEmpty(channel.lineup) ?? '';
final dvrSource = liveTvNonEmpty(channel.liveDvrKey) ?? '';
return '$serverId\u0000$providerSource\u0000$dvrSource';
}
String liveTvChannelSourceLabel(LiveTvChannel channel) {
final serverLabel = _nonEmpty(channel.serverName) ?? _nonEmpty(channel.serverId) ?? 'Live TV';
final sourceTitle = _nonEmpty(channel.liveTvSourceTitle);
final serverLabel = liveTvNonEmpty(channel.serverName) ?? liveTvNonEmpty(channel.serverId) ?? 'Live TV';
final sourceTitle = liveTvNonEmpty(channel.liveTvSourceTitle);
if (sourceTitle == null || sourceTitle == serverLabel) return serverLabel;
return '$serverLabel - $sourceTitle';
}
@@ -62,8 +62,8 @@ String _deduplicatedLabel(LiveTvChannelGroup group) {
if (group.channels.isEmpty) return group.label;
final first = group.channels.first;
final suffixes = [
_nonEmpty(first.liveTvSourceTitle),
_nonEmpty(first.liveDvrKey),
liveTvNonEmpty(first.liveTvSourceTitle),
liveTvNonEmpty(first.liveDvrKey),
liveTvProviderIdentifierForChannel(first),
];
String? suffix;
@@ -76,8 +76,3 @@ String _deduplicatedLabel(LiveTvChannelGroup group) {
if (suffix == null || group.label.contains(suffix)) return group.label;
return '${group.label} - $suffix';
}
String? _nonEmpty(String? value) {
final trimmed = value?.trim();
return trimmed == null || trimmed.isEmpty ? null : trimmed;
}
+10 -9
View File
@@ -2,14 +2,14 @@ import '../models/livetv_channel.dart';
import '../models/livetv_program.dart';
bool liveTvProgramMatchesChannel(LiveTvProgram program, LiveTvChannel channel) {
final programChannel = _nonEmpty(program.channelIdentifier);
final programChannel = liveTvNonEmpty(program.channelIdentifier);
if (programChannel == null) return false;
if (programChannel != channel.key && programChannel != channel.identifier) return false;
if (!_nullableIdsMatch(program.serverId, channel.serverId)) return false;
if (!_nullableIdsMatch(program.liveDvrKey, channel.liveDvrKey)) return false;
final programProvider = _nonEmpty(program.providerIdentifier);
final programProvider = liveTvNonEmpty(program.providerIdentifier);
final channelProvider = liveTvProviderIdentifierForChannel(channel);
if (programProvider != null && channelProvider != null && programProvider != channelProvider) return false;
@@ -17,26 +17,27 @@ bool liveTvProgramMatchesChannel(LiveTvProgram program, LiveTvChannel channel) {
}
String? liveTvProviderIdentifierForChannel(LiveTvChannel channel) {
final source = _nonEmpty(channel.favoriteSource);
final source = liveTvNonEmpty(channel.favoriteSource);
if (source != null) {
final uri = Uri.tryParse(source);
if (uri != null && uri.pathSegments.isNotEmpty) return _nonEmpty(uri.pathSegments.last);
if (uri != null && uri.pathSegments.isNotEmpty) return liveTvNonEmpty(uri.pathSegments.last);
final slashIndex = source.lastIndexOf('/');
if (slashIndex >= 0 && slashIndex < source.length - 1) {
return _nonEmpty(source.substring(slashIndex + 1));
return liveTvNonEmpty(source.substring(slashIndex + 1));
}
}
return _nonEmpty(channel.lineup);
return liveTvNonEmpty(channel.lineup);
}
bool _nullableIdsMatch(String? a, String? b) {
final left = _nonEmpty(a);
final right = _nonEmpty(b);
final left = liveTvNonEmpty(a);
final right = liveTvNonEmpty(b);
return left == null || right == null || left == right;
}
String? _nonEmpty(String? value) {
/// Trimmed [value], or null when it is null, empty, or whitespace-only.
String? liveTvNonEmpty(String? value) {
final trimmed = value?.trim();
return trimmed == null || trimmed.isEmpty ? null : trimmed;
}