mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-05 18:48:38 +02:00
refactor(dart): extract queue search and media policies
This commit is contained in:
@@ -12,6 +12,7 @@ import 'package:spotiflac_android/models/settings.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_state.dart';
|
||||
import 'package:spotiflac_android/services/app_state_database.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/services/download_request_payload.dart';
|
||||
@@ -29,6 +30,7 @@ import 'package:spotiflac_android/utils/progress_stream_poller.dart';
|
||||
import 'package:spotiflac_android/providers/download_history_provider.dart';
|
||||
|
||||
export 'package:spotiflac_android/providers/download_history_provider.dart';
|
||||
export 'package:spotiflac_android/providers/download_queue_state.dart';
|
||||
|
||||
export 'package:spotiflac_android/services/history_database.dart'
|
||||
show HistoryLookupRequest, HistoryBatchLookupRequest;
|
||||
@@ -85,72 +87,6 @@ final _qualityFilenameTokenPattern = RegExp(
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
class DownloadQueueState {
|
||||
static const Object _noChange = Object();
|
||||
final List<DownloadItem> items;
|
||||
final DownloadQueueLookup lookup;
|
||||
final DownloadItem? currentDownload;
|
||||
final bool isProcessing;
|
||||
final bool isPaused;
|
||||
final String outputDir;
|
||||
final String filenameFormat;
|
||||
final String singleFilenameFormat;
|
||||
final String audioQuality;
|
||||
final bool autoFallback;
|
||||
|
||||
const DownloadQueueState({
|
||||
this.items = const [],
|
||||
this.lookup = const DownloadQueueLookup.empty(),
|
||||
this.currentDownload,
|
||||
this.isProcessing = false,
|
||||
this.isPaused = false,
|
||||
this.outputDir = '',
|
||||
this.filenameFormat = '{artist} - {title}',
|
||||
this.singleFilenameFormat = '{title} - {artist}',
|
||||
this.audioQuality = 'LOSSLESS',
|
||||
this.autoFallback = true,
|
||||
});
|
||||
|
||||
DownloadQueueState copyWith({
|
||||
List<DownloadItem>? items,
|
||||
DownloadQueueLookup? lookup,
|
||||
Object? currentDownload = _noChange,
|
||||
bool? isProcessing,
|
||||
bool? isPaused,
|
||||
String? outputDir,
|
||||
String? filenameFormat,
|
||||
String? singleFilenameFormat,
|
||||
String? audioQuality,
|
||||
bool? autoFallback,
|
||||
}) {
|
||||
final resolvedItems = items ?? this.items;
|
||||
return DownloadQueueState(
|
||||
items: resolvedItems,
|
||||
lookup:
|
||||
lookup ??
|
||||
(items != null
|
||||
? DownloadQueueLookup.fromItems(resolvedItems)
|
||||
: this.lookup),
|
||||
currentDownload: identical(currentDownload, _noChange)
|
||||
? this.currentDownload
|
||||
: currentDownload as DownloadItem?,
|
||||
isProcessing: isProcessing ?? this.isProcessing,
|
||||
isPaused: isPaused ?? this.isPaused,
|
||||
outputDir: outputDir ?? this.outputDir,
|
||||
filenameFormat: filenameFormat ?? this.filenameFormat,
|
||||
singleFilenameFormat: singleFilenameFormat ?? this.singleFilenameFormat,
|
||||
audioQuality: audioQuality ?? this.audioQuality,
|
||||
autoFallback: autoFallback ?? this.autoFallback,
|
||||
);
|
||||
}
|
||||
|
||||
int get queuedCount => items.isEmpty ? 0 : lookup.queuedCount;
|
||||
int get completedCount => items.isEmpty ? 0 : lookup.completedCount;
|
||||
int get failedCount => items.isEmpty ? 0 : lookup.failedCount;
|
||||
int get activeDownloadsCount =>
|
||||
items.isEmpty ? 0 : lookup.activeDownloadsCount;
|
||||
}
|
||||
|
||||
class _ProgressUpdate {
|
||||
final DownloadStatus status;
|
||||
final double progress;
|
||||
@@ -4056,177 +3992,6 @@ final downloadQueueProvider =
|
||||
DownloadQueueNotifier.new,
|
||||
);
|
||||
|
||||
class DownloadQueueLookup {
|
||||
final Map<String, DownloadItem> byTrackId;
|
||||
final Map<String, DownloadItem> byItemId;
|
||||
final Map<String, int> indexByItemId;
|
||||
final List<String> itemIds;
|
||||
final int queuedCount;
|
||||
final int completedCount;
|
||||
final int failedCount;
|
||||
final int activeDownloadsCount;
|
||||
final int finalizingCount;
|
||||
|
||||
const DownloadQueueLookup.empty()
|
||||
: byTrackId = const {},
|
||||
byItemId = const {},
|
||||
indexByItemId = const {},
|
||||
itemIds = const [],
|
||||
queuedCount = 0,
|
||||
completedCount = 0,
|
||||
failedCount = 0,
|
||||
activeDownloadsCount = 0,
|
||||
finalizingCount = 0;
|
||||
|
||||
DownloadQueueLookup._({
|
||||
required this.byTrackId,
|
||||
required this.byItemId,
|
||||
required this.indexByItemId,
|
||||
required this.itemIds,
|
||||
required this.queuedCount,
|
||||
required this.completedCount,
|
||||
required this.failedCount,
|
||||
required this.activeDownloadsCount,
|
||||
required this.finalizingCount,
|
||||
});
|
||||
|
||||
factory DownloadQueueLookup.fromItems(List<DownloadItem> items) {
|
||||
final byTrackId = <String, DownloadItem>{};
|
||||
final byItemId = <String, DownloadItem>{};
|
||||
final indexByItemId = <String, int>{};
|
||||
final itemIds = <String>[];
|
||||
var queuedCount = 0;
|
||||
var completedCount = 0;
|
||||
var failedCount = 0;
|
||||
var activeDownloadsCount = 0;
|
||||
var finalizingCount = 0;
|
||||
for (var index = 0; index < items.length; index++) {
|
||||
final item = items[index];
|
||||
byTrackId.putIfAbsent(item.track.id, () => item);
|
||||
byItemId[item.id] = item;
|
||||
indexByItemId[item.id] = index;
|
||||
itemIds.add(item.id);
|
||||
if (_countsAsQueued(item.status)) queuedCount++;
|
||||
if (item.status == DownloadStatus.completed) completedCount++;
|
||||
if (item.status == DownloadStatus.failed) failedCount++;
|
||||
if (item.status == DownloadStatus.downloading) activeDownloadsCount++;
|
||||
if (item.status == DownloadStatus.finalizing) finalizingCount++;
|
||||
}
|
||||
return DownloadQueueLookup._(
|
||||
byTrackId: Map.unmodifiable(byTrackId),
|
||||
byItemId: Map.unmodifiable(byItemId),
|
||||
indexByItemId: Map.unmodifiable(indexByItemId),
|
||||
itemIds: List.unmodifiable(itemIds),
|
||||
queuedCount: queuedCount,
|
||||
completedCount: completedCount,
|
||||
failedCount: failedCount,
|
||||
activeDownloadsCount: activeDownloadsCount,
|
||||
finalizingCount: finalizingCount,
|
||||
);
|
||||
}
|
||||
|
||||
static bool _countsAsQueued(DownloadStatus status) =>
|
||||
status == DownloadStatus.queued ||
|
||||
status == DownloadStatus.downloading ||
|
||||
status == DownloadStatus.finalizing;
|
||||
|
||||
static int _deltaForStatus({
|
||||
required DownloadStatus previous,
|
||||
required DownloadStatus next,
|
||||
required bool Function(DownloadStatus status) predicate,
|
||||
}) {
|
||||
final had = predicate(previous);
|
||||
final has = predicate(next);
|
||||
if (had == has) return 0;
|
||||
return has ? 1 : -1;
|
||||
}
|
||||
|
||||
DownloadQueueLookup updatedForIndices({
|
||||
required List<DownloadItem> previousItems,
|
||||
required List<DownloadItem> nextItems,
|
||||
required Iterable<int> changedIndices,
|
||||
}) {
|
||||
if (previousItems.length != nextItems.length ||
|
||||
itemIds.length != nextItems.length ||
|
||||
indexByItemId.length != nextItems.length) {
|
||||
return DownloadQueueLookup.fromItems(nextItems);
|
||||
}
|
||||
|
||||
final normalizedChanged = <int>[];
|
||||
for (final index in changedIndices) {
|
||||
if (index < 0 || index >= nextItems.length) {
|
||||
return DownloadQueueLookup.fromItems(nextItems);
|
||||
}
|
||||
normalizedChanged.add(index);
|
||||
}
|
||||
if (normalizedChanged.isEmpty) return this;
|
||||
|
||||
var nextQueuedCount = queuedCount;
|
||||
var nextCompletedCount = completedCount;
|
||||
var nextFailedCount = failedCount;
|
||||
var nextActiveDownloadsCount = activeDownloadsCount;
|
||||
var nextFinalizingCount = finalizingCount;
|
||||
Map<String, DownloadItem>? nextByItemId;
|
||||
Map<String, DownloadItem>? nextByTrackId;
|
||||
|
||||
for (final index in normalizedChanged) {
|
||||
final previous = previousItems[index];
|
||||
final next = nextItems[index];
|
||||
if (previous.id != next.id || previous.track.id != next.track.id) {
|
||||
return DownloadQueueLookup.fromItems(nextItems);
|
||||
}
|
||||
|
||||
nextByItemId ??= Map<String, DownloadItem>.from(byItemId);
|
||||
nextByItemId[next.id] = next;
|
||||
if (byTrackId[next.track.id]?.id == previous.id) {
|
||||
nextByTrackId ??= Map<String, DownloadItem>.from(byTrackId);
|
||||
nextByTrackId[next.track.id] = next;
|
||||
}
|
||||
nextQueuedCount += _deltaForStatus(
|
||||
previous: previous.status,
|
||||
next: next.status,
|
||||
predicate: _countsAsQueued,
|
||||
);
|
||||
nextCompletedCount += _deltaForStatus(
|
||||
previous: previous.status,
|
||||
next: next.status,
|
||||
predicate: (status) => status == DownloadStatus.completed,
|
||||
);
|
||||
nextFailedCount += _deltaForStatus(
|
||||
previous: previous.status,
|
||||
next: next.status,
|
||||
predicate: (status) => status == DownloadStatus.failed,
|
||||
);
|
||||
nextActiveDownloadsCount += _deltaForStatus(
|
||||
previous: previous.status,
|
||||
next: next.status,
|
||||
predicate: (status) => status == DownloadStatus.downloading,
|
||||
);
|
||||
nextFinalizingCount += _deltaForStatus(
|
||||
previous: previous.status,
|
||||
next: next.status,
|
||||
predicate: (status) => status == DownloadStatus.finalizing,
|
||||
);
|
||||
}
|
||||
|
||||
return DownloadQueueLookup._(
|
||||
byTrackId: nextByTrackId == null
|
||||
? byTrackId
|
||||
: Map.unmodifiable(nextByTrackId),
|
||||
byItemId: nextByItemId == null
|
||||
? byItemId
|
||||
: Map.unmodifiable(nextByItemId),
|
||||
indexByItemId: indexByItemId,
|
||||
itemIds: itemIds,
|
||||
queuedCount: nextQueuedCount,
|
||||
completedCount: nextCompletedCount,
|
||||
failedCount: nextFailedCount,
|
||||
activeDownloadsCount: nextActiveDownloadsCount,
|
||||
finalizingCount: nextFinalizingCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final downloadQueueLookupProvider = Provider<DownloadQueueLookup>((ref) {
|
||||
return ref.watch(downloadQueueProvider.select((s) => s.lookup));
|
||||
});
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import 'package:spotiflac_android/models/download_item.dart';
|
||||
|
||||
/// Immutable queue state shared by the notifier and read-only UI consumers.
|
||||
///
|
||||
/// Keeping this model outside the notifier implementation makes queue state
|
||||
/// transitions testable without importing the download orchestration pipeline.
|
||||
class DownloadQueueState {
|
||||
static const Object _noChange = Object();
|
||||
|
||||
final List<DownloadItem> items;
|
||||
final DownloadQueueLookup lookup;
|
||||
final DownloadItem? currentDownload;
|
||||
final bool isProcessing;
|
||||
final bool isPaused;
|
||||
final String outputDir;
|
||||
final String filenameFormat;
|
||||
final String singleFilenameFormat;
|
||||
final String audioQuality;
|
||||
final bool autoFallback;
|
||||
|
||||
const DownloadQueueState({
|
||||
this.items = const [],
|
||||
this.lookup = const DownloadQueueLookup.empty(),
|
||||
this.currentDownload,
|
||||
this.isProcessing = false,
|
||||
this.isPaused = false,
|
||||
this.outputDir = '',
|
||||
this.filenameFormat = '{artist} - {title}',
|
||||
this.singleFilenameFormat = '{title} - {artist}',
|
||||
this.audioQuality = 'LOSSLESS',
|
||||
this.autoFallback = true,
|
||||
});
|
||||
|
||||
DownloadQueueState copyWith({
|
||||
List<DownloadItem>? items,
|
||||
DownloadQueueLookup? lookup,
|
||||
Object? currentDownload = _noChange,
|
||||
bool? isProcessing,
|
||||
bool? isPaused,
|
||||
String? outputDir,
|
||||
String? filenameFormat,
|
||||
String? singleFilenameFormat,
|
||||
String? audioQuality,
|
||||
bool? autoFallback,
|
||||
}) {
|
||||
final resolvedItems = items ?? this.items;
|
||||
return DownloadQueueState(
|
||||
items: resolvedItems,
|
||||
lookup:
|
||||
lookup ??
|
||||
(items != null
|
||||
? DownloadQueueLookup.fromItems(resolvedItems)
|
||||
: this.lookup),
|
||||
currentDownload: identical(currentDownload, _noChange)
|
||||
? this.currentDownload
|
||||
: currentDownload as DownloadItem?,
|
||||
isProcessing: isProcessing ?? this.isProcessing,
|
||||
isPaused: isPaused ?? this.isPaused,
|
||||
outputDir: outputDir ?? this.outputDir,
|
||||
filenameFormat: filenameFormat ?? this.filenameFormat,
|
||||
singleFilenameFormat: singleFilenameFormat ?? this.singleFilenameFormat,
|
||||
audioQuality: audioQuality ?? this.audioQuality,
|
||||
autoFallback: autoFallback ?? this.autoFallback,
|
||||
);
|
||||
}
|
||||
|
||||
int get queuedCount => items.isEmpty ? 0 : lookup.queuedCount;
|
||||
int get completedCount => items.isEmpty ? 0 : lookup.completedCount;
|
||||
int get failedCount => items.isEmpty ? 0 : lookup.failedCount;
|
||||
int get activeDownloadsCount =>
|
||||
items.isEmpty ? 0 : lookup.activeDownloadsCount;
|
||||
}
|
||||
|
||||
/// Precomputed queue indexes and counters.
|
||||
///
|
||||
/// The lookup can update in O(changed items) when item identities are stable,
|
||||
/// avoiding full queue scans for every progress event.
|
||||
class DownloadQueueLookup {
|
||||
final Map<String, DownloadItem> byTrackId;
|
||||
final Map<String, DownloadItem> byItemId;
|
||||
final Map<String, int> indexByItemId;
|
||||
final List<String> itemIds;
|
||||
final int queuedCount;
|
||||
final int completedCount;
|
||||
final int failedCount;
|
||||
final int activeDownloadsCount;
|
||||
final int finalizingCount;
|
||||
|
||||
const DownloadQueueLookup.empty()
|
||||
: byTrackId = const {},
|
||||
byItemId = const {},
|
||||
indexByItemId = const {},
|
||||
itemIds = const [],
|
||||
queuedCount = 0,
|
||||
completedCount = 0,
|
||||
failedCount = 0,
|
||||
activeDownloadsCount = 0,
|
||||
finalizingCount = 0;
|
||||
|
||||
DownloadQueueLookup._({
|
||||
required this.byTrackId,
|
||||
required this.byItemId,
|
||||
required this.indexByItemId,
|
||||
required this.itemIds,
|
||||
required this.queuedCount,
|
||||
required this.completedCount,
|
||||
required this.failedCount,
|
||||
required this.activeDownloadsCount,
|
||||
required this.finalizingCount,
|
||||
});
|
||||
|
||||
factory DownloadQueueLookup.fromItems(List<DownloadItem> items) {
|
||||
final byTrackId = <String, DownloadItem>{};
|
||||
final byItemId = <String, DownloadItem>{};
|
||||
final indexByItemId = <String, int>{};
|
||||
final itemIds = <String>[];
|
||||
var queuedCount = 0;
|
||||
var completedCount = 0;
|
||||
var failedCount = 0;
|
||||
var activeDownloadsCount = 0;
|
||||
var finalizingCount = 0;
|
||||
for (var index = 0; index < items.length; index++) {
|
||||
final item = items[index];
|
||||
byTrackId.putIfAbsent(item.track.id, () => item);
|
||||
byItemId[item.id] = item;
|
||||
indexByItemId[item.id] = index;
|
||||
itemIds.add(item.id);
|
||||
if (_countsAsQueued(item.status)) queuedCount++;
|
||||
if (item.status == DownloadStatus.completed) completedCount++;
|
||||
if (item.status == DownloadStatus.failed) failedCount++;
|
||||
if (item.status == DownloadStatus.downloading) activeDownloadsCount++;
|
||||
if (item.status == DownloadStatus.finalizing) finalizingCount++;
|
||||
}
|
||||
return DownloadQueueLookup._(
|
||||
byTrackId: Map.unmodifiable(byTrackId),
|
||||
byItemId: Map.unmodifiable(byItemId),
|
||||
indexByItemId: Map.unmodifiable(indexByItemId),
|
||||
itemIds: List.unmodifiable(itemIds),
|
||||
queuedCount: queuedCount,
|
||||
completedCount: completedCount,
|
||||
failedCount: failedCount,
|
||||
activeDownloadsCount: activeDownloadsCount,
|
||||
finalizingCount: finalizingCount,
|
||||
);
|
||||
}
|
||||
|
||||
static bool _countsAsQueued(DownloadStatus status) =>
|
||||
status == DownloadStatus.queued ||
|
||||
status == DownloadStatus.downloading ||
|
||||
status == DownloadStatus.finalizing;
|
||||
|
||||
static int _deltaForStatus({
|
||||
required DownloadStatus previous,
|
||||
required DownloadStatus next,
|
||||
required bool Function(DownloadStatus status) predicate,
|
||||
}) {
|
||||
final had = predicate(previous);
|
||||
final has = predicate(next);
|
||||
if (had == has) return 0;
|
||||
return has ? 1 : -1;
|
||||
}
|
||||
|
||||
DownloadQueueLookup updatedForIndices({
|
||||
required List<DownloadItem> previousItems,
|
||||
required List<DownloadItem> nextItems,
|
||||
required Iterable<int> changedIndices,
|
||||
}) {
|
||||
if (previousItems.length != nextItems.length ||
|
||||
itemIds.length != nextItems.length ||
|
||||
indexByItemId.length != nextItems.length) {
|
||||
return DownloadQueueLookup.fromItems(nextItems);
|
||||
}
|
||||
|
||||
final normalizedChanged = <int>[];
|
||||
for (final index in changedIndices) {
|
||||
if (index < 0 || index >= nextItems.length) {
|
||||
return DownloadQueueLookup.fromItems(nextItems);
|
||||
}
|
||||
normalizedChanged.add(index);
|
||||
}
|
||||
if (normalizedChanged.isEmpty) return this;
|
||||
|
||||
var nextQueuedCount = queuedCount;
|
||||
var nextCompletedCount = completedCount;
|
||||
var nextFailedCount = failedCount;
|
||||
var nextActiveDownloadsCount = activeDownloadsCount;
|
||||
var nextFinalizingCount = finalizingCount;
|
||||
Map<String, DownloadItem>? nextByItemId;
|
||||
Map<String, DownloadItem>? nextByTrackId;
|
||||
|
||||
for (final index in normalizedChanged) {
|
||||
final previous = previousItems[index];
|
||||
final next = nextItems[index];
|
||||
if (previous.id != next.id || previous.track.id != next.track.id) {
|
||||
return DownloadQueueLookup.fromItems(nextItems);
|
||||
}
|
||||
|
||||
nextByItemId ??= Map<String, DownloadItem>.from(byItemId);
|
||||
nextByItemId[next.id] = next;
|
||||
if (byTrackId[next.track.id]?.id == previous.id) {
|
||||
nextByTrackId ??= Map<String, DownloadItem>.from(byTrackId);
|
||||
nextByTrackId[next.track.id] = next;
|
||||
}
|
||||
nextQueuedCount += _deltaForStatus(
|
||||
previous: previous.status,
|
||||
next: next.status,
|
||||
predicate: _countsAsQueued,
|
||||
);
|
||||
nextCompletedCount += _deltaForStatus(
|
||||
previous: previous.status,
|
||||
next: next.status,
|
||||
predicate: (status) => status == DownloadStatus.completed,
|
||||
);
|
||||
nextFailedCount += _deltaForStatus(
|
||||
previous: previous.status,
|
||||
next: next.status,
|
||||
predicate: (status) => status == DownloadStatus.failed,
|
||||
);
|
||||
nextActiveDownloadsCount += _deltaForStatus(
|
||||
previous: previous.status,
|
||||
next: next.status,
|
||||
predicate: (status) => status == DownloadStatus.downloading,
|
||||
);
|
||||
nextFinalizingCount += _deltaForStatus(
|
||||
previous: previous.status,
|
||||
next: next.status,
|
||||
predicate: (status) => status == DownloadStatus.finalizing,
|
||||
);
|
||||
}
|
||||
|
||||
return DownloadQueueLookup._(
|
||||
byTrackId: nextByTrackId == null
|
||||
? byTrackId
|
||||
: Map.unmodifiable(nextByTrackId),
|
||||
byItemId: nextByItemId == null
|
||||
? byItemId
|
||||
: Map.unmodifiable(nextByItemId),
|
||||
indexByItemId: indexByItemId,
|
||||
itemIds: itemIds,
|
||||
queuedCount: nextQueuedCount,
|
||||
completedCount: nextCompletedCount,
|
||||
failedCount: nextFailedCount,
|
||||
activeDownloadsCount: nextActiveDownloadsCount,
|
||||
finalizingCount: nextFinalizingCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user