diff --git a/lib/components/track_presentation/presentation_actions.dart b/lib/components/track_presentation/presentation_actions.dart index 197d3dca..735a4514 100644 --- a/lib/components/track_presentation/presentation_actions.dart +++ b/lib/components/track_presentation/presentation_actions.dart @@ -15,49 +15,53 @@ import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +ToastOverlay showToastForAction( + BuildContext context, + String action, + int count, +) { + final message = switch (action) { + "download" => (context.l10n.download_count(count), SpotubeIcons.download), + "add-to-playlist" => ( + context.l10n.add_count_to_playlist(count), + SpotubeIcons.playlistAdd + ), + "add-to-queue" => ( + context.l10n.add_count_to_queue(count), + SpotubeIcons.queueAdd + ), + "play-next" => ( + context.l10n.play_count_next(count), + SpotubeIcons.lightning + ), + _ => ("", SpotubeIcons.error), + }; + + return showToast( + context: context, + location: ToastLocation.topRight, + builder: (context, overlay) { + return SurfaceCard( + child: Basic( + leading: Icon(message.$2), + title: Text(message.$1), + leadingAlignment: Alignment.center, + trailing: IconButton.ghost( + size: ButtonSize.small, + icon: const Icon(SpotubeIcons.close), + onPressed: () { + overlay.close(); + }, + ), + ), + ); + }, + ); +} + class TrackPresentationActionsSection extends HookConsumerWidget { const TrackPresentationActionsSection({super.key}); - showToastForAction(BuildContext context, String action, int count) { - final message = switch (action) { - "download" => (context.l10n.download_count(count), SpotubeIcons.download), - "add-to-playlist" => ( - context.l10n.add_count_to_playlist(count), - SpotubeIcons.playlistAdd - ), - "add-to-queue" => ( - context.l10n.add_count_to_queue(count), - SpotubeIcons.queueAdd - ), - "play-next" => ( - context.l10n.play_count_next(count), - SpotubeIcons.lightning - ), - _ => ("", SpotubeIcons.error), - }; - - showToast( - context: context, - location: ToastLocation.topRight, - builder: (context, overlay) { - return SurfaceCard( - child: Basic( - leading: Icon(message.$2), - title: Text(message.$1), - leadingAlignment: Alignment.center, - trailing: IconButton.ghost( - size: ButtonSize.small, - icon: const Icon(SpotubeIcons.close), - onPressed: () { - overlay.close(); - }, - ), - ), - ); - }, - ); - } - @override Widget build(BuildContext context, ref) { final options = TrackPresentationOptions.of(context); diff --git a/lib/components/track_presentation/presentation_top.dart b/lib/components/track_presentation/presentation_top.dart index ae2be910..d2576cc0 100644 --- a/lib/components/track_presentation/presentation_top.dart +++ b/lib/components/track_presentation/presentation_top.dart @@ -30,7 +30,7 @@ class TrackPresentationTopSection extends HookConsumerWidget { final imageDimension = mediaQuery.mdAndUp ? 200 : 120; - final (:isLoading, :isActive, :onPlay, :onShuffle) = + final (:isLoading, :isActive, :onPlay, :onShuffle, :onAddToQueue) = useActionCallbacks(ref); final playbackActions = Row( @@ -59,15 +59,15 @@ class TrackPresentationTopSection extends HookConsumerWidget { child: IconButton.secondary( icon: const Icon(SpotubeIcons.queueAdd), enabled: !isLoading && !isActive, - onPressed: () {}, + onPressed: onAddToQueue, ), ) else Button.secondary( leading: const Icon(SpotubeIcons.add), enabled: !isLoading && !isActive, + onPressed: onAddToQueue, child: Text(context.l10n.queue), - onPressed: () {}, ), Button.primary( alignment: Alignment.center, diff --git a/lib/components/track_presentation/use_action_callbacks.dart b/lib/components/track_presentation/use_action_callbacks.dart index ff49f4a4..22f60ded 100644 --- a/lib/components/track_presentation/use_action_callbacks.dart +++ b/lib/components/track_presentation/use_action_callbacks.dart @@ -1,8 +1,10 @@ import 'dart:math'; +import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.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/models/connect/connect.dart'; @@ -17,6 +19,7 @@ typedef UseActionCallbacks = ({ bool isLoading, Future Function() onShuffle, Future Function() onPlay, + VoidCallback onAddToQueue, }); UseActionCallbacks useActionCallbacks(WidgetRef ref) { @@ -96,6 +99,7 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) { if (isRemoteDevice == null) return; if (isRemoteDevice) { final allTracks = await options.pagination.onFetchAll(); + final remotePlayback = ref.read(connectProvider.notifier); await remotePlayback.load( options.collection is SpotubeSimpleAlbumObject @@ -109,14 +113,19 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) { ), ); } else { + if (initialTracks.isEmpty) return; + await playlistNotifier.load(initialTracks, autoPlay: true); playlistNotifier.addCollection(options.collectionId); + if (options.collection is SpotubeSimpleAlbumObject) { - historyNotifier - .addAlbums([options.collection as SpotubeSimpleAlbumObject]); + historyNotifier.addAlbums( + [options.collection as SpotubeSimpleAlbumObject], + ); } else { historyNotifier.addPlaylists( - [options.collection as SpotubeSimplePlaylistObject]); + [options.collection as SpotubeSimplePlaylistObject], + ); } final allTracks = await options.pagination.onFetchAll(); @@ -132,10 +141,26 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) { } }, [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 ( isActive: isActive, isLoading: isLoading.value, onShuffle: onShuffle, onPlay: onPlay, + onAddToQueue: onAddToQueue, ); } diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index d77cb9a4..b6811873 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -144,16 +144,21 @@ class AudioPlayerNotifier extends Notifier { }), audioPlayer.playlistStream.listen((playlist) async { 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 .map((media) => TrackSourceQuery.parseUri(media.uri)) .toList(); + final trackGroupedById = groupBy( + state.tracks, + (query) => query.id, + ); + final tracks = queries - .map( - (query) => state.tracks.firstWhereOrNull( - (element) => element.id == query.id, - ), - ) + .map((query) => trackGroupedById[query.id]?.firstOrNull) .nonNulls .toList(); @@ -269,12 +274,12 @@ class AudioPlayerNotifier extends Notifier { _assertAllowedTracks(tracks); tracks = _blacklist.filter(tracks).toList(); - for (final track in tracks) { - await audioPlayer.addTrack(SpotubeMedia(track)); - } state = state.copyWith( tracks: [...state.tracks, ...tracks], ); + for (final track in tracks) { + await audioPlayer.addTrack(SpotubeMedia(track)); + } } Future removeTrack(String trackId) async {