mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-05 18:48:38 +02:00
fix(download): complete quality variants reliably
This commit is contained in:
@@ -74,6 +74,15 @@ Future<void> persistBeforePublishingDownloadCompletion({
|
||||
publish();
|
||||
}
|
||||
|
||||
/// Keeps native-worker finalization visually distinct from completion.
|
||||
/// Download bytes may reach 100% before SAF publishing, metadata persistence,
|
||||
/// and queue reconciliation have finished.
|
||||
double nativeWorkerFinalizingProgress(double progress) {
|
||||
final normalized = progress.clamp(0.0, 1.0).toDouble();
|
||||
if (normalized <= 0) return 0.95;
|
||||
return min(normalized, 0.99);
|
||||
}
|
||||
|
||||
final _invalidFolderChars = RegExp(r'[<>:"/\\|?*]');
|
||||
final _trimDotsAndSpacesRegex = RegExp(r'^[. ]+|[. ]+$');
|
||||
final _trimUnderscoresAndSpacesRegex = RegExp(r'^[_ ]+|[_ ]+$');
|
||||
@@ -1488,7 +1497,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
_verificationRetryGuard.retainItems(remainingIds);
|
||||
_rateLimitRetriedItemIds.removeWhere((id) => !remainingIds.contains(id));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final downloadQueueProvider =
|
||||
|
||||
@@ -803,7 +803,7 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
updateItemStatus(
|
||||
itemId,
|
||||
DownloadStatus.finalizing,
|
||||
progress: progress <= 0 ? 0.95 : progress,
|
||||
progress: nativeWorkerFinalizingProgress(progress),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -811,12 +811,34 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
if (status == 'completed') {
|
||||
final result = itemSnapshot['result'];
|
||||
if (result is Map) {
|
||||
reconciledIds.add(itemId);
|
||||
await _completeAndroidNativeWorkerItem(
|
||||
context,
|
||||
Map<String, dynamic>.from(result),
|
||||
settings,
|
||||
);
|
||||
try {
|
||||
await _completeAndroidNativeWorkerItem(
|
||||
context,
|
||||
Map<String, dynamic>.from(result),
|
||||
settings,
|
||||
);
|
||||
reconciledIds.add(itemId);
|
||||
} catch (e, stack) {
|
||||
// A native output can be complete while Library persistence or
|
||||
// Dart-side adoption fails. Do not leave the queue permanently at
|
||||
// 100%: surface the reconciliation failure and keep processing
|
||||
// the rest of the worker batch.
|
||||
_log.e(
|
||||
'Failed to reconcile completed native worker item $itemId: $e',
|
||||
e,
|
||||
stack,
|
||||
);
|
||||
if (_findItemById(itemId) != null) {
|
||||
updateItemStatus(
|
||||
itemId,
|
||||
DownloadStatus.failed,
|
||||
error: 'Downloaded file could not be added to the Library: $e',
|
||||
errorType: DownloadErrorType.unknown,
|
||||
);
|
||||
_failedInSession++;
|
||||
}
|
||||
reconciledIds.add(itemId);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -537,7 +537,12 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen>
|
||||
child: TrackListTile(
|
||||
track: track,
|
||||
isInHistory: isInHistory,
|
||||
onDownload: () => _downloadTrack(context, track),
|
||||
onDownload: ({bool forceQualityPicker = false}) =>
|
||||
_downloadTrack(
|
||||
context,
|
||||
track,
|
||||
forceQualityPicker: forceQualityPicker,
|
||||
),
|
||||
clickableArtist: true,
|
||||
leading: SizedBox(
|
||||
width: 32,
|
||||
@@ -559,12 +564,17 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen>
|
||||
);
|
||||
}
|
||||
|
||||
void _downloadTrack(BuildContext context, Track track) {
|
||||
void _downloadTrack(
|
||||
BuildContext context,
|
||||
Track track, {
|
||||
bool forceQualityPicker = false,
|
||||
}) {
|
||||
downloadSingleTrack(
|
||||
context,
|
||||
ref,
|
||||
track,
|
||||
recommendedService: _recommendedDownloadService(),
|
||||
forceQualityPicker: forceQualityPicker,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -423,7 +423,12 @@ extension _ArtistScreenSections on _ArtistScreenState {
|
||||
final isQueued = queueItem != null;
|
||||
|
||||
return InkWell(
|
||||
onTap: () => _handlePopularTrackTap(track, isQueued: isQueued),
|
||||
onTap: () => _handlePopularTrackTap(
|
||||
track,
|
||||
isQueued: isQueued,
|
||||
isInHistory: isInHistory,
|
||||
isInLocalLibrary: isInLocalLibrary,
|
||||
),
|
||||
onLongPress: () => TrackCollectionQuickActions.showTrackOptionsSheet(
|
||||
context,
|
||||
ref,
|
||||
@@ -531,9 +536,20 @@ extension _ArtistScreenSections on _ArtistScreenState {
|
||||
);
|
||||
}
|
||||
|
||||
void _handlePopularTrackTap(Track track, {required bool isQueued}) async {
|
||||
void _handlePopularTrackTap(
|
||||
Track track, {
|
||||
required bool isQueued,
|
||||
required bool isInHistory,
|
||||
required bool isInLocalLibrary,
|
||||
}) async {
|
||||
if (isQueued) return;
|
||||
|
||||
final settings = ref.read(settingsProvider);
|
||||
if (settings.allowQualityVariants && (isInHistory || isInLocalLibrary)) {
|
||||
_downloadTrack(track);
|
||||
return;
|
||||
}
|
||||
|
||||
final playedLocal = await playLocalIfAvailable(context, ref, track);
|
||||
if (playedLocal) {
|
||||
return;
|
||||
|
||||
@@ -323,8 +323,13 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen>
|
||||
child: TrackListTile(
|
||||
track: track,
|
||||
isInHistory: isInHistory,
|
||||
onDownload: () =>
|
||||
_downloadTrack(context, track, playlistPosition: index + 1),
|
||||
onDownload: ({bool forceQualityPicker = false}) =>
|
||||
_downloadTrack(
|
||||
context,
|
||||
track,
|
||||
playlistPosition: index + 1,
|
||||
forceQualityPicker: forceQualityPicker,
|
||||
),
|
||||
leading: track.coverUrl != null
|
||||
? CachedCoverImage(
|
||||
imageUrl: track.coverUrl!,
|
||||
@@ -358,6 +363,7 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen>
|
||||
BuildContext context,
|
||||
Track track, {
|
||||
int? playlistPosition,
|
||||
bool forceQualityPicker = false,
|
||||
}) {
|
||||
downloadSingleTrack(
|
||||
context,
|
||||
@@ -366,6 +372,7 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen>
|
||||
recommendedService: _recommendedDownloadService(),
|
||||
playlistName: _playlistName,
|
||||
playlistPosition: playlistPosition,
|
||||
forceQualityPicker: forceQualityPicker,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,14 +11,15 @@ import 'package:spotiflac_android/widgets/in_library_badge.dart';
|
||||
import 'package:spotiflac_android/widgets/preview_button.dart';
|
||||
import 'package:spotiflac_android/widgets/track_collection_quick_actions.dart';
|
||||
|
||||
/// Track row shared by the album and playlist screens. Tap plays the local
|
||||
/// copy when one exists and otherwise triggers [onDownload]; long-press opens
|
||||
/// the track options sheet. Callers supply [leading] (track number, cover
|
||||
/// art, ...) and choose whether the artist name links to the artist screen.
|
||||
/// Track row shared by the album and playlist screens. Tap offers another
|
||||
/// download when quality variants are enabled, otherwise it plays the local
|
||||
/// copy when one exists and falls back to [onDownload]. Long-press opens the
|
||||
/// track options sheet. Callers supply [leading] (track number, cover art,
|
||||
/// ...) and choose whether the artist name links to the artist screen.
|
||||
class TrackListTile extends ConsumerWidget {
|
||||
final Track track;
|
||||
final bool isInHistory;
|
||||
final VoidCallback onDownload;
|
||||
final void Function({bool forceQualityPicker}) onDownload;
|
||||
final Widget leading;
|
||||
final bool clickableArtist;
|
||||
|
||||
@@ -118,7 +119,12 @@ class TrackListTile extends ConsumerWidget {
|
||||
TrackCollectionQuickActions(track: track),
|
||||
],
|
||||
),
|
||||
onTap: () => _handleTap(context, ref, isQueued: isQueued),
|
||||
onTap: () => _handleTap(
|
||||
context,
|
||||
ref,
|
||||
isQueued: isQueued,
|
||||
isInLocalLibrary: isInLocalLibrary,
|
||||
),
|
||||
onLongPress: () => TrackCollectionQuickActions.showTrackOptionsSheet(
|
||||
context,
|
||||
ref,
|
||||
@@ -133,9 +139,16 @@ class TrackListTile extends ConsumerWidget {
|
||||
BuildContext context,
|
||||
WidgetRef ref, {
|
||||
required bool isQueued,
|
||||
required bool isInLocalLibrary,
|
||||
}) async {
|
||||
if (isQueued) return;
|
||||
|
||||
final settings = ref.read(settingsProvider);
|
||||
if (settings.allowQualityVariants && (isInHistory || isInLocalLibrary)) {
|
||||
onDownload(forceQualityPicker: true);
|
||||
return;
|
||||
}
|
||||
|
||||
final playedLocal = await playLocalIfAvailable(context, ref, track);
|
||||
if (playedLocal) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user