mirror of
https://github.com/spotiflacapp/SpotiFLAC-Mobile.git
synced 2026-06-01 03:15:17 +07:00
feat: update collection actions for offline-first playback
This commit is contained in:
parent
54a7b6b568
commit
a07c125454
4 changed files with 450 additions and 165 deletions
|
|
@ -8,6 +8,7 @@ import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
|||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/providers/recent_access_provider.dart';
|
||||
import 'package:spotiflac_android/providers/local_library_provider.dart';
|
||||
import 'package:spotiflac_android/providers/playback_provider.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/utils/file_access.dart';
|
||||
import 'package:spotiflac_android/widgets/track_collection_quick_actions.dart';
|
||||
|
|
@ -761,7 +762,12 @@ class _AlbumTrackItem extends ConsumerWidget {
|
|||
|
||||
final isInHistory = ref.watch(
|
||||
downloadHistoryProvider.select((state) {
|
||||
return state.isDownloaded(track.id);
|
||||
if (state.isDownloaded(track.id)) return true;
|
||||
final isrc = track.isrc?.trim();
|
||||
if (isrc != null && isrc.isNotEmpty && state.getByIsrc(isrc) != null) {
|
||||
return true;
|
||||
}
|
||||
return state.findByTrackAndArtist(track.name, track.artistName) != null;
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -861,13 +867,7 @@ class _AlbumTrackItem extends ConsumerWidget {
|
|||
],
|
||||
),
|
||||
trailing: TrackCollectionQuickActions(track: track),
|
||||
onTap: () => _handleTap(
|
||||
context,
|
||||
ref,
|
||||
isQueued: isQueued,
|
||||
isInHistory: isInHistory,
|
||||
isInLocalLibrary: isInLocalLibrary,
|
||||
),
|
||||
onTap: () => _handleTap(context, ref, isQueued: isQueued),
|
||||
onLongPress: () => TrackCollectionQuickActions.showTrackOptionsSheet(
|
||||
context,
|
||||
ref,
|
||||
|
|
@ -882,47 +882,84 @@ class _AlbumTrackItem extends ConsumerWidget {
|
|||
BuildContext context,
|
||||
WidgetRef ref, {
|
||||
required bool isQueued,
|
||||
required bool isInHistory,
|
||||
required bool isInLocalLibrary,
|
||||
}) async {
|
||||
if (isQueued) return;
|
||||
|
||||
if (isInLocalLibrary) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.snackbarAlreadyInLibrary(track.name)),
|
||||
),
|
||||
);
|
||||
}
|
||||
final playedLocal = await _playLocalIfAvailable(context, ref);
|
||||
if (playedLocal) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInHistory) {
|
||||
final historyItem = ref
|
||||
.read(downloadHistoryProvider.notifier)
|
||||
.getBySpotifyId(track.id);
|
||||
if (historyItem != null) {
|
||||
final exists = await fileExists(historyItem.filePath);
|
||||
if (exists) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.snackbarAlreadyDownloaded(track.name),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
ref
|
||||
.read(downloadHistoryProvider.notifier)
|
||||
.removeBySpotifyId(track.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDownload();
|
||||
}
|
||||
|
||||
Future<bool> _playLocalIfAvailable(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
) async {
|
||||
final localState = ref.read(localLibraryProvider);
|
||||
final historyState = ref.read(downloadHistoryProvider);
|
||||
final historyNotifier = ref.read(downloadHistoryProvider.notifier);
|
||||
|
||||
try {
|
||||
DownloadHistoryItem? historyItem = historyNotifier.getBySpotifyId(
|
||||
track.id,
|
||||
);
|
||||
final isrc = track.isrc?.trim();
|
||||
historyItem ??= (isrc != null && isrc.isNotEmpty)
|
||||
? historyNotifier.getByIsrc(isrc)
|
||||
: null;
|
||||
historyItem ??= historyState.findByTrackAndArtist(
|
||||
track.name,
|
||||
track.artistName,
|
||||
);
|
||||
|
||||
if (historyItem != null) {
|
||||
final exists = await fileExists(historyItem.filePath);
|
||||
if (exists) {
|
||||
await ref
|
||||
.read(playbackProvider.notifier)
|
||||
.playLocalPath(
|
||||
path: historyItem.filePath,
|
||||
title: track.name,
|
||||
artist: track.artistName,
|
||||
album: track.albumName,
|
||||
coverUrl: track.coverUrl ?? '',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
historyNotifier.removeFromHistory(historyItem.id);
|
||||
}
|
||||
|
||||
var localItem = (isrc != null && isrc.isNotEmpty)
|
||||
? localState.getByIsrc(isrc)
|
||||
: null;
|
||||
localItem ??= localState.findByTrackAndArtist(
|
||||
track.name,
|
||||
track.artistName,
|
||||
);
|
||||
|
||||
if (localItem != null && await fileExists(localItem.filePath)) {
|
||||
await ref
|
||||
.read(playbackProvider.notifier)
|
||||
.playLocalPath(
|
||||
path: localItem.filePath,
|
||||
title: localItem.trackName,
|
||||
artist: localItem.artistName,
|
||||
album: localItem.albumName,
|
||||
coverUrl: localItem.coverPath ?? track.coverUrl ?? '',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.snackbarCannotOpenFile('$e'))),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'package:spotiflac_android/providers/settings_provider.dart';
|
|||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
import 'package:spotiflac_android/providers/recent_access_provider.dart';
|
||||
import 'package:spotiflac_android/providers/local_library_provider.dart';
|
||||
import 'package:spotiflac_android/providers/playback_provider.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/utils/file_access.dart';
|
||||
import 'package:spotiflac_android/screens/album_screen.dart';
|
||||
|
|
@ -1243,9 +1244,17 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
|||
);
|
||||
|
||||
final isInHistory = ref.watch(
|
||||
downloadHistoryProvider.select(
|
||||
(state) => state.isDownloaded(track.id),
|
||||
),
|
||||
downloadHistoryProvider.select((state) {
|
||||
if (state.isDownloaded(track.id)) return true;
|
||||
final isrc = track.isrc?.trim();
|
||||
if (isrc != null &&
|
||||
isrc.isNotEmpty &&
|
||||
state.getByIsrc(isrc) != null) {
|
||||
return true;
|
||||
}
|
||||
return state.findByTrackAndArtist(track.name, track.artistName) !=
|
||||
null;
|
||||
}),
|
||||
);
|
||||
|
||||
final showLocalLibraryIndicator = ref.watch(
|
||||
|
|
@ -1268,12 +1277,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
|||
final isQueued = queueItem != null;
|
||||
|
||||
return InkWell(
|
||||
onTap: () => _handlePopularTrackTap(
|
||||
track,
|
||||
isQueued: isQueued,
|
||||
isInHistory: isInHistory,
|
||||
isInLocalLibrary: isInLocalLibrary,
|
||||
),
|
||||
onTap: () => _handlePopularTrackTap(track, isQueued: isQueued),
|
||||
onLongPress: () => TrackCollectionQuickActions.showTrackOptionsSheet(
|
||||
context,
|
||||
ref,
|
||||
|
|
@ -1344,17 +1348,61 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
|||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (track.albumName.isNotEmpty)
|
||||
ClickableAlbumName(
|
||||
albumName: track.albumName,
|
||||
albumId: track.albumId,
|
||||
artistName: track.artistName,
|
||||
coverUrl: track.coverUrl,
|
||||
extensionId: widget.extensionId,
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
if (track.albumName.isNotEmpty ||
|
||||
isInLocalLibrary ||
|
||||
isInHistory)
|
||||
Row(
|
||||
children: [
|
||||
if (track.albumName.isNotEmpty)
|
||||
Expanded(
|
||||
child: ClickableAlbumName(
|
||||
albumName: track.albumName,
|
||||
albumId: track.albumId,
|
||||
artistName: track.artistName,
|
||||
coverUrl: track.coverUrl,
|
||||
extensionId: widget.extensionId,
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (isInLocalLibrary || isInHistory) ...[
|
||||
if (track.albumName.isNotEmpty)
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.tertiaryContainer,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.folder_outlined,
|
||||
size: 10,
|
||||
color: colorScheme.onTertiaryContainer,
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Text(
|
||||
context.l10n.libraryInLibrary,
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colorScheme.onTertiaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -1369,51 +1417,82 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
|||
}
|
||||
|
||||
/// Handle tap on popular track item
|
||||
void _handlePopularTrackTap(
|
||||
Track track, {
|
||||
required bool isQueued,
|
||||
required bool isInHistory,
|
||||
required bool isInLocalLibrary,
|
||||
}) async {
|
||||
void _handlePopularTrackTap(Track track, {required bool isQueued}) async {
|
||||
if (isQueued) return;
|
||||
|
||||
if (isInLocalLibrary) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.snackbarAlreadyInLibrary(track.name)),
|
||||
),
|
||||
);
|
||||
}
|
||||
final playedLocal = await _playLocalIfAvailable(track);
|
||||
if (playedLocal) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInHistory) {
|
||||
final historyItem = ref
|
||||
.read(downloadHistoryProvider.notifier)
|
||||
.getBySpotifyId(track.id);
|
||||
_downloadTrack(track);
|
||||
}
|
||||
|
||||
Future<bool> _playLocalIfAvailable(Track track) async {
|
||||
final localState = ref.read(localLibraryProvider);
|
||||
final historyState = ref.read(downloadHistoryProvider);
|
||||
final historyNotifier = ref.read(downloadHistoryProvider.notifier);
|
||||
|
||||
try {
|
||||
DownloadHistoryItem? historyItem = historyNotifier.getBySpotifyId(
|
||||
track.id,
|
||||
);
|
||||
final isrc = track.isrc?.trim();
|
||||
historyItem ??= (isrc != null && isrc.isNotEmpty)
|
||||
? historyNotifier.getByIsrc(isrc)
|
||||
: null;
|
||||
historyItem ??= historyState.findByTrackAndArtist(
|
||||
track.name,
|
||||
track.artistName,
|
||||
);
|
||||
|
||||
if (historyItem != null) {
|
||||
final exists = await fileExists(historyItem.filePath);
|
||||
if (exists) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.snackbarAlreadyDownloaded(track.name),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
ref
|
||||
.read(downloadHistoryProvider.notifier)
|
||||
.removeBySpotifyId(track.id);
|
||||
await ref
|
||||
.read(playbackProvider.notifier)
|
||||
.playLocalPath(
|
||||
path: historyItem.filePath,
|
||||
title: track.name,
|
||||
artist: track.artistName,
|
||||
album: track.albumName,
|
||||
coverUrl: track.coverUrl ?? '',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
historyNotifier.removeFromHistory(historyItem.id);
|
||||
}
|
||||
|
||||
var localItem = (isrc != null && isrc.isNotEmpty)
|
||||
? localState.getByIsrc(isrc)
|
||||
: null;
|
||||
localItem ??= localState.findByTrackAndArtist(
|
||||
track.name,
|
||||
track.artistName,
|
||||
);
|
||||
|
||||
if (localItem != null && await fileExists(localItem.filePath)) {
|
||||
await ref
|
||||
.read(playbackProvider.notifier)
|
||||
.playLocalPath(
|
||||
path: localItem.filePath,
|
||||
title: localItem.trackName,
|
||||
artist: localItem.artistName,
|
||||
album: localItem.albumName,
|
||||
coverUrl: localItem.coverPath ?? track.coverUrl ?? '',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.snackbarCannotOpenFile('$e'))),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_downloadTrack(track);
|
||||
return false;
|
||||
}
|
||||
|
||||
void _downloadTrack(Track track) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:spotiflac_android/providers/library_collections_provider.dart';
|
|||
import 'package:spotiflac_android/utils/file_access.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/providers/local_library_provider.dart';
|
||||
import 'package:spotiflac_android/providers/playback_provider.dart';
|
||||
import 'package:spotiflac_android/widgets/download_service_picker.dart';
|
||||
import 'package:spotiflac_android/widgets/playlist_picker_sheet.dart';
|
||||
import 'package:spotiflac_android/widgets/track_collection_quick_actions.dart';
|
||||
|
|
@ -631,7 +632,12 @@ class _PlaylistTrackItem extends ConsumerWidget {
|
|||
|
||||
final isInHistory = ref.watch(
|
||||
downloadHistoryProvider.select((state) {
|
||||
return state.isDownloaded(track.id);
|
||||
if (state.isDownloaded(track.id)) return true;
|
||||
final isrc = track.isrc?.trim();
|
||||
if (isrc != null && isrc.isNotEmpty && state.getByIsrc(isrc) != null) {
|
||||
return true;
|
||||
}
|
||||
return state.findByTrackAndArtist(track.name, track.artistName) != null;
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -742,13 +748,7 @@ class _PlaylistTrackItem extends ConsumerWidget {
|
|||
],
|
||||
),
|
||||
trailing: TrackCollectionQuickActions(track: track),
|
||||
onTap: () => _handleTap(
|
||||
context,
|
||||
ref,
|
||||
isQueued: isQueued,
|
||||
isInHistory: isInHistory,
|
||||
isInLocalLibrary: isInLocalLibrary,
|
||||
),
|
||||
onTap: () => _handleTap(context, ref, isQueued: isQueued),
|
||||
onLongPress: () => TrackCollectionQuickActions.showTrackOptionsSheet(
|
||||
context,
|
||||
ref,
|
||||
|
|
@ -763,47 +763,84 @@ class _PlaylistTrackItem extends ConsumerWidget {
|
|||
BuildContext context,
|
||||
WidgetRef ref, {
|
||||
required bool isQueued,
|
||||
required bool isInHistory,
|
||||
required bool isInLocalLibrary,
|
||||
}) async {
|
||||
if (isQueued) return;
|
||||
|
||||
if (isInLocalLibrary) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.snackbarAlreadyInLibrary(track.name)),
|
||||
),
|
||||
);
|
||||
}
|
||||
final playedLocal = await _playLocalIfAvailable(context, ref);
|
||||
if (playedLocal) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInHistory) {
|
||||
final historyItem = ref
|
||||
.read(downloadHistoryProvider.notifier)
|
||||
.getBySpotifyId(track.id);
|
||||
if (historyItem != null) {
|
||||
final exists = await fileExists(historyItem.filePath);
|
||||
if (exists) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.snackbarAlreadyDownloaded(track.name),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
ref
|
||||
.read(downloadHistoryProvider.notifier)
|
||||
.removeBySpotifyId(track.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDownload();
|
||||
}
|
||||
|
||||
Future<bool> _playLocalIfAvailable(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
) async {
|
||||
final localState = ref.read(localLibraryProvider);
|
||||
final historyState = ref.read(downloadHistoryProvider);
|
||||
final historyNotifier = ref.read(downloadHistoryProvider.notifier);
|
||||
|
||||
try {
|
||||
DownloadHistoryItem? historyItem = historyNotifier.getBySpotifyId(
|
||||
track.id,
|
||||
);
|
||||
final isrc = track.isrc?.trim();
|
||||
historyItem ??= (isrc != null && isrc.isNotEmpty)
|
||||
? historyNotifier.getByIsrc(isrc)
|
||||
: null;
|
||||
historyItem ??= historyState.findByTrackAndArtist(
|
||||
track.name,
|
||||
track.artistName,
|
||||
);
|
||||
|
||||
if (historyItem != null) {
|
||||
final exists = await fileExists(historyItem.filePath);
|
||||
if (exists) {
|
||||
await ref
|
||||
.read(playbackProvider.notifier)
|
||||
.playLocalPath(
|
||||
path: historyItem.filePath,
|
||||
title: track.name,
|
||||
artist: track.artistName,
|
||||
album: track.albumName,
|
||||
coverUrl: track.coverUrl ?? '',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
historyNotifier.removeFromHistory(historyItem.id);
|
||||
}
|
||||
|
||||
var localItem = (isrc != null && isrc.isNotEmpty)
|
||||
? localState.getByIsrc(isrc)
|
||||
: null;
|
||||
localItem ??= localState.findByTrackAndArtist(
|
||||
track.name,
|
||||
track.artistName,
|
||||
);
|
||||
|
||||
if (localItem != null && await fileExists(localItem.filePath)) {
|
||||
await ref
|
||||
.read(playbackProvider.notifier)
|
||||
.playLocalPath(
|
||||
path: localItem.filePath,
|
||||
title: localItem.trackName,
|
||||
artist: localItem.artistName,
|
||||
album: localItem.albumName,
|
||||
coverUrl: localItem.coverPath ?? track.coverUrl ?? '',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.snackbarCannotOpenFile('$e'))),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
|
@ -5,8 +7,11 @@ import 'package:spotiflac_android/l10n/l10n.dart';
|
|||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
import 'package:spotiflac_android/providers/library_collections_provider.dart';
|
||||
import 'package:spotiflac_android/providers/local_library_provider.dart';
|
||||
import 'package:spotiflac_android/providers/playback_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/services/cover_cache_manager.dart';
|
||||
import 'package:spotiflac_android/utils/file_access.dart';
|
||||
import 'package:spotiflac_android/widgets/download_service_picker.dart';
|
||||
import 'package:spotiflac_android/widgets/playlist_picker_sheet.dart';
|
||||
import 'package:spotiflac_android/utils/clickable_metadata.dart';
|
||||
|
|
@ -171,9 +176,20 @@ class _TrackOptionsSheet extends ConsumerWidget {
|
|||
// Action items (matches _QualityOption style)
|
||||
_OptionTile(
|
||||
icon: Icons.download_rounded,
|
||||
title: context.l10n.downloadTitle,
|
||||
title: 'Download & Play',
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
final playedLocal = await _playLocalIfAvailable(
|
||||
container,
|
||||
rootContext,
|
||||
);
|
||||
if (playedLocal) {
|
||||
return;
|
||||
}
|
||||
if (!rootContext.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.askQualityBeforeDownload) {
|
||||
DownloadServicePicker.show(
|
||||
rootContext,
|
||||
|
|
@ -181,35 +197,19 @@ class _TrackOptionsSheet extends ConsumerWidget {
|
|||
artistName: track.artistName,
|
||||
coverUrl: track.coverUrl,
|
||||
onSelect: (quality, service) {
|
||||
container
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addToQueue(
|
||||
track,
|
||||
service,
|
||||
qualityOverride: quality,
|
||||
);
|
||||
ScaffoldMessenger.of(rootContext).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
rootContext.l10n.snackbarAddedToQueue(track.name),
|
||||
),
|
||||
),
|
||||
_enqueueDownloadAndAutoPlay(
|
||||
container: container,
|
||||
context: rootContext,
|
||||
service: service,
|
||||
quality: quality,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
container
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addToQueue(track, settings.defaultService);
|
||||
if (!rootContext.mounted) {
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(rootContext).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
rootContext.l10n.snackbarAddedToQueue(track.name),
|
||||
),
|
||||
),
|
||||
_enqueueDownloadAndAutoPlay(
|
||||
container: container,
|
||||
context: rootContext,
|
||||
service: settings.defaultService,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
@ -282,6 +282,138 @@ class _TrackOptionsSheet extends ConsumerWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _playLocalIfAvailable(
|
||||
ProviderContainer container,
|
||||
BuildContext context,
|
||||
) async {
|
||||
final localState = container.read(localLibraryProvider);
|
||||
final historyState = container.read(downloadHistoryProvider);
|
||||
final historyNotifier = container.read(downloadHistoryProvider.notifier);
|
||||
|
||||
try {
|
||||
DownloadHistoryItem? historyItem = historyNotifier.getBySpotifyId(
|
||||
track.id,
|
||||
);
|
||||
final isrc = track.isrc?.trim();
|
||||
historyItem ??= (isrc != null && isrc.isNotEmpty)
|
||||
? historyNotifier.getByIsrc(isrc)
|
||||
: null;
|
||||
historyItem ??= historyState.findByTrackAndArtist(
|
||||
track.name,
|
||||
track.artistName,
|
||||
);
|
||||
|
||||
if (historyItem != null) {
|
||||
final exists = await fileExists(historyItem.filePath);
|
||||
if (exists) {
|
||||
await container
|
||||
.read(playbackProvider.notifier)
|
||||
.playLocalPath(
|
||||
path: historyItem.filePath,
|
||||
title: track.name,
|
||||
artist: track.artistName,
|
||||
album: track.albumName,
|
||||
coverUrl: track.coverUrl ?? '',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
historyNotifier.removeFromHistory(historyItem.id);
|
||||
}
|
||||
|
||||
var localItem = (isrc != null && isrc.isNotEmpty)
|
||||
? localState.getByIsrc(isrc)
|
||||
: null;
|
||||
localItem ??= localState.findByTrackAndArtist(
|
||||
track.name,
|
||||
track.artistName,
|
||||
);
|
||||
|
||||
if (localItem != null && await fileExists(localItem.filePath)) {
|
||||
await container
|
||||
.read(playbackProvider.notifier)
|
||||
.playLocalPath(
|
||||
path: localItem.filePath,
|
||||
title: localItem.trackName,
|
||||
artist: localItem.artistName,
|
||||
album: localItem.albumName,
|
||||
coverUrl: localItem.coverPath ?? track.coverUrl ?? '',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.snackbarCannotOpenFile('$e'))),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void _enqueueDownloadAndAutoPlay({
|
||||
required ProviderContainer container,
|
||||
required BuildContext context,
|
||||
required String service,
|
||||
String? quality,
|
||||
}) {
|
||||
container
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addToQueue(track, service, qualityOverride: quality);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))),
|
||||
);
|
||||
}
|
||||
unawaited(_waitForDownloadedFileAndPlay(container, context));
|
||||
}
|
||||
|
||||
Future<void> _waitForDownloadedFileAndPlay(
|
||||
ProviderContainer container,
|
||||
BuildContext context,
|
||||
) async {
|
||||
const maxAttempts = 180; // up to ~3 minutes
|
||||
for (var i = 0; i < maxAttempts; i++) {
|
||||
final item = _findHistoryMatch(container);
|
||||
if (item != null && await fileExists(item.filePath)) {
|
||||
try {
|
||||
await container
|
||||
.read(playbackProvider.notifier)
|
||||
.playLocalPath(
|
||||
path: item.filePath,
|
||||
title: track.name,
|
||||
artist: track.artistName,
|
||||
album: track.albumName,
|
||||
coverUrl: track.coverUrl ?? '',
|
||||
);
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.snackbarCannotOpenFile('$e')),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
}
|
||||
|
||||
DownloadHistoryItem? _findHistoryMatch(ProviderContainer container) {
|
||||
final historyState = container.read(downloadHistoryProvider);
|
||||
final historyNotifier = container.read(downloadHistoryProvider.notifier);
|
||||
final isrc = track.isrc?.trim();
|
||||
|
||||
return historyNotifier.getBySpotifyId(track.id) ??
|
||||
((isrc != null && isrc.isNotEmpty)
|
||||
? historyNotifier.getByIsrc(isrc)
|
||||
: null) ??
|
||||
historyState.findByTrackAndArtist(track.name, track.artistName);
|
||||
}
|
||||
}
|
||||
|
||||
/// Styled like _QualityOption in download_service_picker.dart
|
||||
|
|
|
|||
Loading…
Reference in a new issue