From 441b43bef6b92fd7df6c4e1bef39d67b4a76cd22 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 28 Apr 2023 10:57:32 +0600 Subject: [PATCH] feat(queue): reorder tracks support --- lib/collections/spotube_icons.dart | 1 + lib/components/player/player_queue.dart | 14 +- .../shared/track_table/track_tile.dart | 222 +++++++++--------- lib/provider/playlist_queue_provider.dart | 12 + lib/themes/theme.dart | 5 + 5 files changed, 140 insertions(+), 114 deletions(-) diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 767fb505a..b98b67852 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -74,4 +74,5 @@ abstract class SpotubeIcons { static const pinOff = Icons.push_pin_outlined; static const hoverOn = Icons.back_hand_rounded; static const hoverOff = Icons.back_hand_outlined; + static const dragHandle = Icons.drag_indicator; } diff --git a/lib/components/player/player_queue.dart b/lib/components/player/player_queue.dart index a1fe5d766..f66075a7d 100644 --- a/lib/components/player/player_queue.dart +++ b/lib/components/player/player_queue.dart @@ -110,10 +110,14 @@ class PlayerQueue extends HookConsumerWidget { ), const SizedBox(height: 10), Flexible( - child: ListView.builder( - controller: controller, + child: ReorderableListView.builder( + onReorder: (oldIndex, newIndex) { + playlistNotifier.reorder(oldIndex, newIndex); + }, + scrollController: controller, itemCount: tracks.length, shrinkWrap: true, + buildDefaultDragHandles: false, itemBuilder: (context, i) { final track = tracks.toList().asMap().entries.elementAt(i); String duration = @@ -135,6 +139,12 @@ class PlayerQueue extends HookConsumerWidget { } await playlistNotifier.playTrack(currentTrack); }, + leadingActions: [ + ReorderableDragStartListener( + index: i, + child: const Icon(SpotubeIcons.dragHandle), + ), + ], ), ), ); diff --git a/lib/components/shared/track_table/track_tile.dart b/lib/components/shared/track_table/track_tile.dart index fad1a60e5..34e95af04 100644 --- a/lib/components/shared/track_table/track_tile.dart +++ b/lib/components/shared/track_table/track_tile.dart @@ -41,6 +41,7 @@ class TrackTile extends HookConsumerWidget { final void Function(bool?)? onCheckChange; final List? actions; + final List? leadingActions; TrackTile( this.playlist, { @@ -56,6 +57,7 @@ class TrackTile extends HookConsumerWidget { this.isLocal = false, this.onCheckChange, this.actions, + this.leadingActions, Key? key, }) : super(key: key); @@ -190,6 +192,7 @@ class TrackTile extends HookConsumerWidget { type: MaterialType.transparency, child: Row( children: [ + ...?leadingActions, if (showCheck && !isBlackListed) Checkbox( value: isChecked, @@ -300,132 +303,127 @@ class TrackTile extends HookConsumerWidget { Text(duration), ], const SizedBox(width: 10), - PopupMenuButton( - icon: const Icon(SpotubeIcons.moreHorizontal), - elevation: 4, - position: PopupMenuPosition.under, - tooltip: "More options", - itemBuilder: (context) { - return [ - if (!playlistQueueNotifier.isTrackOnQueue(track.value)) - PopupMenuItem( - padding: EdgeInsets.zero, - onTap: () { - playlistQueueNotifier.add([track.value]); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Added ${track.value.name} to queue"), - ), - ); - }, - child: const ListTile( - leading: Icon(SpotubeIcons.queueAdd), - title: Text("Add to queue"), + if (!isLocal) + PopupMenuButton( + icon: const Icon(SpotubeIcons.moreHorizontal), + position: PopupMenuPosition.under, + tooltip: "More options", + itemBuilder: (context) { + return [ + if (!playlistQueueNotifier.isTrackOnQueue(track.value)) + PopupMenuItem( + padding: EdgeInsets.zero, + onTap: () { + playlistQueueNotifier.add([track.value]); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text("Added ${track.value.name} to queue"), + ), + ); + }, + child: const ListTile( + leading: Icon(SpotubeIcons.queueAdd), + title: Text("Add to queue"), + ), + ) + else + PopupMenuItem( + padding: EdgeInsets.zero, + onTap: () { + playlistQueueNotifier.remove([track.value]); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "Removed ${track.value.name} from queue"), + ), + ); + }, + child: const ListTile( + leading: Icon(SpotubeIcons.queueRemove), + title: Text("Remove from queue"), + ), ), - ) - else - PopupMenuItem( - padding: EdgeInsets.zero, - onTap: () { - playlistQueueNotifier.remove([track.value]); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - Text("Removed ${track.value.name} from queue"), - ), - ); - }, - child: const ListTile( - leading: Icon(SpotubeIcons.queueRemove), - title: Text("Remove from queue"), + if (toggler.item3.hasData) + PopupMenuItem( + padding: EdgeInsets.zero, + onTap: () { + toggler.item2.mutate(toggler.item1); + }, + child: ListTile( + leading: toggler.item1 + ? const Icon( + SpotubeIcons.heartFilled, + color: Colors.pink, + ) + : const Icon(SpotubeIcons.heart), + title: const Text("Save as favorite"), + ), + ), + if (auth != null) + PopupMenuItem( + padding: EdgeInsets.zero, + onTap: actionAddToPlaylist, + child: const ListTile( + leading: Icon(SpotubeIcons.playlistAdd), + title: Text("Add To playlist"), + ), + ), + if (userPlaylist && auth != null) + PopupMenuItem( + padding: EdgeInsets.zero, + onTap: () { + removingTrack.value = track.value.uri; + removeTrack.mutate(track.value.uri!); + }, + child: ListTile( + leading: (removeTrack.isMutating || + !removeTrack.hasData) && + removingTrack.value == track.value.uri + ? const Center( + child: CircularProgressIndicator(), + ) + : const Icon(SpotubeIcons.removeFilled), + title: const Text("Remove from playlist"), + ), ), - ), - if (toggler.item3.hasData) PopupMenuItem( padding: EdgeInsets.zero, onTap: () { - toggler.item2.mutate(toggler.item1); + if (isBlackListed) { + ref.read(BlackListNotifier.provider.notifier).remove( + BlacklistedElement.track( + track.value.id!, track.value.name!), + ); + } else { + ref.read(BlackListNotifier.provider.notifier).add( + BlacklistedElement.track( + track.value.id!, track.value.name!), + ); + } }, child: ListTile( - leading: toggler.item1 - ? const Icon( - SpotubeIcons.heartFilled, - color: Colors.pink, - ) - : const Icon(SpotubeIcons.heart), - title: const Text("Save as favorite"), - ), - ), - if (auth != null) - PopupMenuItem( - padding: EdgeInsets.zero, - onTap: actionAddToPlaylist, - child: const ListTile( - leading: Icon(SpotubeIcons.playlistAdd), - title: Text("Add To playlist"), + leading: const Icon(SpotubeIcons.playlistRemove), + iconColor: !isBlackListed ? Colors.red[400] : null, + textColor: !isBlackListed ? Colors.red[400] : null, + title: Text( + "${isBlackListed ? "Remove from" : "Add to"} blacklist", + ), ), ), - if (userPlaylist && auth != null) PopupMenuItem( padding: EdgeInsets.zero, onTap: () { - removingTrack.value = track.value.uri; - removeTrack.mutate(track.value.uri!); + actionShare(track.value); }, - child: ListTile( - leading: - (removeTrack.isMutating || !removeTrack.hasData) && - removingTrack.value == track.value.uri - ? const Center( - child: CircularProgressIndicator(), - ) - : const Icon(SpotubeIcons.removeFilled), - title: const Text("Remove from playlist"), - ), - ), - PopupMenuItem( - padding: EdgeInsets.zero, - onTap: () { - actionShare(track.value); - }, - child: const ListTile( - leading: Icon(SpotubeIcons.share), - title: Text("Share"), - ), - ), - PopupMenuItem( - padding: EdgeInsets.zero, - onTap: () { - if (isBlackListed) { - ref.read(BlackListNotifier.provider.notifier).remove( - BlacklistedElement.track( - track.value.id!, track.value.name!), - ); - } else { - ref.read(BlackListNotifier.provider.notifier).add( - BlacklistedElement.track( - track.value.id!, track.value.name!), - ); - } - }, - child: ListTile( - leading: Icon( - SpotubeIcons.playlistRemove, - color: isBlackListed ? Colors.white : Colors.red[400], - ), - iconColor: isBlackListed ? Colors.red[400] : null, - textColor: isBlackListed ? Colors.red[400] : null, - title: Text( - "${isBlackListed ? "Remove from" : "Add to"} blacklist", - style: TextStyle( - color: isBlackListed ? Colors.white : Colors.red[400], - ), + child: const ListTile( + leading: Icon(SpotubeIcons.share), + title: Text("Share"), ), - ), - ) - ]; - }, - ), + ) + ]; + }, + ), ...?actions, ], ), diff --git a/lib/provider/playlist_queue_provider.dart b/lib/provider/playlist_queue_provider.dart index f4a3b4914..0748ff915 100644 --- a/lib/provider/playlist_queue_provider.dart +++ b/lib/provider/playlist_queue_provider.dart @@ -198,6 +198,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { // skip all the activeTrack.skipSegments if (state?.isLoading != true && + state?.activeTrack is SpotubeTrack && (state?.activeTrack as SpotubeTrack?)?.skipSegments.isNotEmpty == true && preferences.skipSponsorSegments) { @@ -508,6 +509,17 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { return trackIds.contains(track.id!); } + void reorder(int oldIndex, int newIndex) { + if (!isLoaded) return; + + final tracks = state!.tracks.toList(); + final track = tracks.removeAt(oldIndex); + tracks.insert(newIndex, track); + final active = + tracks.indexWhere((element) => element.id == state!.activeTrack.id); + state = state!.copyWith(tracks: Set.from(tracks), active: active); + } + @override Future? fromJson(Map json) { if (json.isEmpty) return null; diff --git a/lib/themes/theme.dart b/lib/themes/theme.dart index d40e2018c..e56a6f504 100644 --- a/lib/themes/theme.dart +++ b/lib/themes/theme.dart @@ -36,5 +36,10 @@ ThemeData theme(Color seed, Brightness brightness) { borderRadius: BorderRadius.circular(15), ), ), + popupMenuTheme: PopupMenuThemeData( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + color: scheme.surface, + elevation: 4, + ), ); }