chore: fix queue order

This commit is contained in:
Kingkor Roy Tirtho 2025-07-19 21:39:31 +06:00
parent cfda46e07e
commit 0a604a9ad5
4 changed files with 88 additions and 54 deletions

View File

@ -15,10 +15,11 @@ import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
class TrackPresentationActionsSection extends HookConsumerWidget { ToastOverlay showToastForAction(
const TrackPresentationActionsSection({super.key}); BuildContext context,
String action,
showToastForAction(BuildContext context, String action, int count) { int count,
) {
final message = switch (action) { final message = switch (action) {
"download" => (context.l10n.download_count(count), SpotubeIcons.download), "download" => (context.l10n.download_count(count), SpotubeIcons.download),
"add-to-playlist" => ( "add-to-playlist" => (
@ -36,7 +37,7 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
_ => ("", SpotubeIcons.error), _ => ("", SpotubeIcons.error),
}; };
showToast( return showToast(
context: context, context: context,
location: ToastLocation.topRight, location: ToastLocation.topRight,
builder: (context, overlay) { builder: (context, overlay) {
@ -58,6 +59,9 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
); );
} }
class TrackPresentationActionsSection extends HookConsumerWidget {
const TrackPresentationActionsSection({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final options = TrackPresentationOptions.of(context); final options = TrackPresentationOptions.of(context);

View File

@ -30,7 +30,7 @@ class TrackPresentationTopSection extends HookConsumerWidget {
final imageDimension = mediaQuery.mdAndUp ? 200 : 120; final imageDimension = mediaQuery.mdAndUp ? 200 : 120;
final (:isLoading, :isActive, :onPlay, :onShuffle) = final (:isLoading, :isActive, :onPlay, :onShuffle, :onAddToQueue) =
useActionCallbacks(ref); useActionCallbacks(ref);
final playbackActions = Row( final playbackActions = Row(
@ -59,15 +59,15 @@ class TrackPresentationTopSection extends HookConsumerWidget {
child: IconButton.secondary( child: IconButton.secondary(
icon: const Icon(SpotubeIcons.queueAdd), icon: const Icon(SpotubeIcons.queueAdd),
enabled: !isLoading && !isActive, enabled: !isLoading && !isActive,
onPressed: () {}, onPressed: onAddToQueue,
), ),
) )
else else
Button.secondary( Button.secondary(
leading: const Icon(SpotubeIcons.add), leading: const Icon(SpotubeIcons.add),
enabled: !isLoading && !isActive, enabled: !isLoading && !isActive,
onPressed: onAddToQueue,
child: Text(context.l10n.queue), child: Text(context.l10n.queue),
onPressed: () {},
), ),
Button.primary( Button.primary(
alignment: Alignment.center, alignment: Alignment.center,

View File

@ -1,8 +1,10 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/dialogs/select_device_dialog.dart'; import 'package:spotube/components/dialogs/select_device_dialog.dart';
import 'package:spotube/components/track_presentation/presentation_actions.dart';
import 'package:spotube/components/track_presentation/presentation_props.dart'; import 'package:spotube/components/track_presentation/presentation_props.dart';
import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/connect/connect.dart';
@ -17,6 +19,7 @@ typedef UseActionCallbacks = ({
bool isLoading, bool isLoading,
Future<void> Function() onShuffle, Future<void> Function() onShuffle,
Future<void> Function() onPlay, Future<void> Function() onPlay,
VoidCallback onAddToQueue,
}); });
UseActionCallbacks useActionCallbacks(WidgetRef ref) { UseActionCallbacks useActionCallbacks(WidgetRef ref) {
@ -96,6 +99,7 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) {
if (isRemoteDevice == null) return; if (isRemoteDevice == null) return;
if (isRemoteDevice) { if (isRemoteDevice) {
final allTracks = await options.pagination.onFetchAll(); final allTracks = await options.pagination.onFetchAll();
final remotePlayback = ref.read(connectProvider.notifier); final remotePlayback = ref.read(connectProvider.notifier);
await remotePlayback.load( await remotePlayback.load(
options.collection is SpotubeSimpleAlbumObject options.collection is SpotubeSimpleAlbumObject
@ -109,14 +113,19 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) {
), ),
); );
} else { } else {
if (initialTracks.isEmpty) return;
await playlistNotifier.load(initialTracks, autoPlay: true); await playlistNotifier.load(initialTracks, autoPlay: true);
playlistNotifier.addCollection(options.collectionId); playlistNotifier.addCollection(options.collectionId);
if (options.collection is SpotubeSimpleAlbumObject) { if (options.collection is SpotubeSimpleAlbumObject) {
historyNotifier historyNotifier.addAlbums(
.addAlbums([options.collection as SpotubeSimpleAlbumObject]); [options.collection as SpotubeSimpleAlbumObject],
);
} else { } else {
historyNotifier.addPlaylists( historyNotifier.addPlaylists(
[options.collection as SpotubeSimplePlaylistObject]); [options.collection as SpotubeSimplePlaylistObject],
);
} }
final allTracks = await options.pagination.onFetchAll(); final allTracks = await options.pagination.onFetchAll();
@ -132,10 +141,26 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) {
} }
}, [options, playlistNotifier, historyNotifier]); }, [options, playlistNotifier, historyNotifier]);
final onAddToQueue = useCallback(() {
final tracks = options.tracks;
playlistNotifier.addTracks(tracks);
playlistNotifier.addCollection(options.collectionId);
if (options.collection is SpotubeSimpleAlbumObject) {
historyNotifier
.addAlbums([options.collection as SpotubeSimpleAlbumObject]);
} else {
historyNotifier
.addPlaylists([options.collection as SpotubeSimplePlaylistObject]);
}
if (!context.mounted) return;
showToastForAction(context, "add-to-queue", tracks.length);
}, [options, playlistNotifier, historyNotifier]);
return ( return (
isActive: isActive, isActive: isActive,
isLoading: isLoading.value, isLoading: isLoading.value,
onShuffle: onShuffle, onShuffle: onShuffle,
onPlay: onPlay, onPlay: onPlay,
onAddToQueue: onAddToQueue,
); );
} }

View File

@ -144,16 +144,21 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
}), }),
audioPlayer.playlistStream.listen((playlist) async { audioPlayer.playlistStream.listen((playlist) async {
try { try {
// Playlist and state has to be in sync. This is only meant for
// the shuffle/re-ordering indices to be in sync
if (playlist.medias.length != state.tracks.length) return;
final queries = playlist.medias final queries = playlist.medias
.map((media) => TrackSourceQuery.parseUri(media.uri)) .map((media) => TrackSourceQuery.parseUri(media.uri))
.toList(); .toList();
final trackGroupedById = groupBy(
state.tracks,
(query) => query.id,
);
final tracks = queries final tracks = queries
.map( .map((query) => trackGroupedById[query.id]?.firstOrNull)
(query) => state.tracks.firstWhereOrNull(
(element) => element.id == query.id,
),
)
.nonNulls .nonNulls
.toList(); .toList();
@ -269,12 +274,12 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
_assertAllowedTracks(tracks); _assertAllowedTracks(tracks);
tracks = _blacklist.filter(tracks).toList(); tracks = _blacklist.filter(tracks).toList();
for (final track in tracks) {
await audioPlayer.addTrack(SpotubeMedia(track));
}
state = state.copyWith( state = state.copyWith(
tracks: [...state.tracks, ...tracks], tracks: [...state.tracks, ...tracks],
); );
for (final track in tracks) {
await audioPlayer.addTrack(SpotubeMedia(track));
}
} }
Future<void> removeTrack(String trackId) async { Future<void> removeTrack(String trackId) async {