mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 21:29:36 +02:00
fix(player): preserve playlist context when playing a track
Queue the full available Library collection from the selected row instead of replacing playback with one track. Preserve playlist order and map the selected position after skipping unavailable and virtual CUE entries, so Next and Previous retain their context. Cover both themes, repeated tracks, list boundaries, unavailable audio, and external-player behavior. Refs #592.
This commit is contained in:
1 parent
34bdbaeecf
commit
87321c7efd
3 files changed
+317
-22
No files matched your search
@@ -145,20 +145,24 @@ class PlaybackController extends Notifier<PlaybackState> {
|
||||
Future<void> playTrackList(List<Track> tracks, {int startIndex = 0}) async {
|
||||
if (tracks.isEmpty) return;
|
||||
|
||||
final orderedTracks = _orderedTracksFromStartIndex(tracks, startIndex);
|
||||
final resolvedPaths = await _resolveTrackPaths(orderedTracks);
|
||||
final safeStart = startIndex.clamp(0, tracks.length - 1);
|
||||
final resolvedPaths = await resolveTrackFilePaths(tracks);
|
||||
|
||||
if (await _useInternalPlayer()) {
|
||||
final queue = <PlayableMedia>[];
|
||||
int? initialIndex;
|
||||
var skippedCueVirtualTrack = false;
|
||||
for (var index = 0; index < orderedTracks.length; index++) {
|
||||
final track = orderedTracks[index];
|
||||
for (var index = 0; index < tracks.length; index++) {
|
||||
final track = tracks[index];
|
||||
final resolvedPath = resolvedPaths[index];
|
||||
if (resolvedPath == null) continue;
|
||||
if (isCueVirtualPath(resolvedPath)) {
|
||||
skippedCueVirtualTrack = true;
|
||||
continue;
|
||||
}
|
||||
// Keep the playlist's original order so Previous can reach earlier
|
||||
// tracks and reaching its end still respects the player's repeat mode.
|
||||
if (index >= safeStart) initialIndex ??= queue.length;
|
||||
queue.add(
|
||||
PlayableMedia(
|
||||
id: resolvedPath,
|
||||
@@ -177,7 +181,9 @@ class PlaybackController extends Notifier<PlaybackState> {
|
||||
|
||||
if (queue.isNotEmpty) {
|
||||
_log.d('Playing ${queue.length} tracks in the internal player');
|
||||
await ref.read(musicPlayerControllerProvider).playAll(queue);
|
||||
await ref
|
||||
.read(musicPlayerControllerProvider)
|
||||
.playAll(queue, initialIndex: initialIndex ?? 0);
|
||||
return;
|
||||
}
|
||||
if (skippedCueVirtualTrack) {
|
||||
@@ -189,8 +195,9 @@ class PlaybackController extends Notifier<PlaybackState> {
|
||||
}
|
||||
|
||||
var skippedCueVirtualTrack = false;
|
||||
for (var index = 0; index < orderedTracks.length; index++) {
|
||||
final track = orderedTracks[index];
|
||||
for (var offset = 0; offset < tracks.length; offset++) {
|
||||
final index = (safeStart + offset) % tracks.length;
|
||||
final track = tracks[index];
|
||||
final resolvedPath = resolvedPaths[index];
|
||||
if (resolvedPath == null) {
|
||||
continue;
|
||||
@@ -222,18 +229,6 @@ class PlaybackController extends Notifier<PlaybackState> {
|
||||
Future<List<String?>> resolveTrackFilePaths(List<Track> tracks) =>
|
||||
_resolveTrackPaths(tracks);
|
||||
|
||||
List<Track> _orderedTracksFromStartIndex(List<Track> tracks, int startIndex) {
|
||||
final safeStart = startIndex.clamp(0, tracks.length - 1);
|
||||
if (safeStart == 0) {
|
||||
return List<Track>.from(tracks, growable: false);
|
||||
}
|
||||
|
||||
return <Track>[
|
||||
...tracks.sublist(safeStart),
|
||||
...tracks.sublist(0, safeStart),
|
||||
];
|
||||
}
|
||||
|
||||
Future<List<String?>> _resolveTrackPaths(List<Track> tracks) async {
|
||||
if (tracks.isEmpty) return const [];
|
||||
final localFuture = LibraryDatabase.instance.findExistingBatch([
|
||||
|
||||
@@ -351,6 +351,7 @@ class _LibraryTracksFolderScreenState
|
||||
mode: widget.mode,
|
||||
playlistId: widget.playlistId,
|
||||
folderTracks: folderTracks,
|
||||
trackIndex: index,
|
||||
isInHistory: isInHistory,
|
||||
isSelectionMode: isSelectionMode,
|
||||
isSelected: isSelected,
|
||||
@@ -910,6 +911,7 @@ class _CollectionTrackTile extends ConsumerWidget {
|
||||
final LibraryTracksFolderMode mode;
|
||||
final String? playlistId;
|
||||
final List<Track> folderTracks;
|
||||
final int trackIndex;
|
||||
final bool isInHistory;
|
||||
final bool isSelectionMode;
|
||||
final bool isSelected;
|
||||
@@ -921,6 +923,7 @@ class _CollectionTrackTile extends ConsumerWidget {
|
||||
required this.mode,
|
||||
required this.playlistId,
|
||||
required this.folderTracks,
|
||||
required this.trackIndex,
|
||||
required this.isInHistory,
|
||||
this.isSelectionMode = false,
|
||||
this.isSelected = false,
|
||||
@@ -1023,9 +1026,7 @@ class _CollectionTrackTile extends ConsumerWidget {
|
||||
trailing: isInHistory || isInLocalLibrary
|
||||
? IconButton(
|
||||
tooltip: context.l10n.tooltipPlay,
|
||||
onPressed: () {
|
||||
ref.read(playbackProvider.notifier).playTrackList([track]);
|
||||
},
|
||||
onPressed: () => _playFromHere(context, ref),
|
||||
icon: Icon(Icons.play_arrow, color: colorScheme.primary),
|
||||
style: IconButton.styleFrom(
|
||||
minimumSize: Size.square(context.tokens.minTouchTarget),
|
||||
@@ -1049,6 +1050,23 @@ class _CollectionTrackTile extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _playFromHere(BuildContext context, WidgetRef ref) async {
|
||||
try {
|
||||
await ref
|
||||
.read(playbackProvider.notifier)
|
||||
.playTrackList(folderTracks, startIndex: trackIndex);
|
||||
} catch (error) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.snackbarCannotOpenFile(context.friendlyError(error)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildTrackCover(BuildContext context, String coverUrl, double size) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
Widget placeholder() => Container(
|
||||
|
||||
Reference in new issue
Block a user