feat(queue): reorder tracks support

This commit is contained in:
Kingkor Roy Tirtho 2023-04-28 10:57:32 +06:00
parent c1d67153ce
commit 441b43bef6
5 changed files with 144 additions and 118 deletions

View File

@ -74,4 +74,5 @@ abstract class SpotubeIcons {
static const pinOff = Icons.push_pin_outlined; static const pinOff = Icons.push_pin_outlined;
static const hoverOn = Icons.back_hand_rounded; static const hoverOn = Icons.back_hand_rounded;
static const hoverOff = Icons.back_hand_outlined; static const hoverOff = Icons.back_hand_outlined;
static const dragHandle = Icons.drag_indicator;
} }

View File

@ -110,10 +110,14 @@ class PlayerQueue extends HookConsumerWidget {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Flexible( Flexible(
child: ListView.builder( child: ReorderableListView.builder(
controller: controller, onReorder: (oldIndex, newIndex) {
playlistNotifier.reorder(oldIndex, newIndex);
},
scrollController: controller,
itemCount: tracks.length, itemCount: tracks.length,
shrinkWrap: true, shrinkWrap: true,
buildDefaultDragHandles: false,
itemBuilder: (context, i) { itemBuilder: (context, i) {
final track = tracks.toList().asMap().entries.elementAt(i); final track = tracks.toList().asMap().entries.elementAt(i);
String duration = String duration =
@ -135,6 +139,12 @@ class PlayerQueue extends HookConsumerWidget {
} }
await playlistNotifier.playTrack(currentTrack); await playlistNotifier.playTrack(currentTrack);
}, },
leadingActions: [
ReorderableDragStartListener(
index: i,
child: const Icon(SpotubeIcons.dragHandle),
),
],
), ),
), ),
); );

View File

@ -41,6 +41,7 @@ class TrackTile extends HookConsumerWidget {
final void Function(bool?)? onCheckChange; final void Function(bool?)? onCheckChange;
final List<Widget>? actions; final List<Widget>? actions;
final List<Widget>? leadingActions;
TrackTile( TrackTile(
this.playlist, { this.playlist, {
@ -56,6 +57,7 @@ class TrackTile extends HookConsumerWidget {
this.isLocal = false, this.isLocal = false,
this.onCheckChange, this.onCheckChange,
this.actions, this.actions,
this.leadingActions,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -190,6 +192,7 @@ class TrackTile extends HookConsumerWidget {
type: MaterialType.transparency, type: MaterialType.transparency,
child: Row( child: Row(
children: [ children: [
...?leadingActions,
if (showCheck && !isBlackListed) if (showCheck && !isBlackListed)
Checkbox( Checkbox(
value: isChecked, value: isChecked,
@ -300,9 +303,9 @@ class TrackTile extends HookConsumerWidget {
Text(duration), Text(duration),
], ],
const SizedBox(width: 10), const SizedBox(width: 10),
if (!isLocal)
PopupMenuButton( PopupMenuButton(
icon: const Icon(SpotubeIcons.moreHorizontal), icon: const Icon(SpotubeIcons.moreHorizontal),
elevation: 4,
position: PopupMenuPosition.under, position: PopupMenuPosition.under,
tooltip: "More options", tooltip: "More options",
itemBuilder: (context) { itemBuilder: (context) {
@ -314,7 +317,8 @@ class TrackTile extends HookConsumerWidget {
playlistQueueNotifier.add([track.value]); playlistQueueNotifier.add([track.value]);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text("Added ${track.value.name} to queue"), content:
Text("Added ${track.value.name} to queue"),
), ),
); );
}, },
@ -330,8 +334,8 @@ class TrackTile extends HookConsumerWidget {
playlistQueueNotifier.remove([track.value]); playlistQueueNotifier.remove([track.value]);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: content: Text(
Text("Removed ${track.value.name} from queue"), "Removed ${track.value.name} from queue"),
), ),
); );
}, },
@ -373,8 +377,8 @@ class TrackTile extends HookConsumerWidget {
removeTrack.mutate(track.value.uri!); removeTrack.mutate(track.value.uri!);
}, },
child: ListTile( child: ListTile(
leading: leading: (removeTrack.isMutating ||
(removeTrack.isMutating || !removeTrack.hasData) && !removeTrack.hasData) &&
removingTrack.value == track.value.uri removingTrack.value == track.value.uri
? const Center( ? const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
@ -383,16 +387,6 @@ class TrackTile extends HookConsumerWidget {
title: const Text("Remove from playlist"), 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( PopupMenuItem(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
onTap: () { onTap: () {
@ -409,19 +403,23 @@ class TrackTile extends HookConsumerWidget {
} }
}, },
child: ListTile( child: ListTile(
leading: Icon( leading: const Icon(SpotubeIcons.playlistRemove),
SpotubeIcons.playlistRemove, iconColor: !isBlackListed ? Colors.red[400] : null,
color: isBlackListed ? Colors.white : Colors.red[400], textColor: !isBlackListed ? Colors.red[400] : null,
),
iconColor: isBlackListed ? Colors.red[400] : null,
textColor: isBlackListed ? Colors.red[400] : null,
title: Text( title: Text(
"${isBlackListed ? "Remove from" : "Add to"} blacklist", "${isBlackListed ? "Remove from" : "Add to"} blacklist",
style: TextStyle(
color: isBlackListed ? Colors.white : Colors.red[400],
), ),
), ),
), ),
PopupMenuItem(
padding: EdgeInsets.zero,
onTap: () {
actionShare(track.value);
},
child: const ListTile(
leading: Icon(SpotubeIcons.share),
title: Text("Share"),
),
) )
]; ];
}, },

View File

@ -198,6 +198,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
// skip all the activeTrack.skipSegments // skip all the activeTrack.skipSegments
if (state?.isLoading != true && if (state?.isLoading != true &&
state?.activeTrack is SpotubeTrack &&
(state?.activeTrack as SpotubeTrack?)?.skipSegments.isNotEmpty == (state?.activeTrack as SpotubeTrack?)?.skipSegments.isNotEmpty ==
true && true &&
preferences.skipSponsorSegments) { preferences.skipSponsorSegments) {
@ -508,6 +509,17 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
return trackIds.contains(track.id!); 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 @override
Future<PlaylistQueue>? fromJson(Map<String, dynamic> json) { Future<PlaylistQueue>? fromJson(Map<String, dynamic> json) {
if (json.isEmpty) return null; if (json.isEmpty) return null;

View File

@ -36,5 +36,10 @@ ThemeData theme(Color seed, Brightness brightness) {
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
), ),
), ),
popupMenuTheme: PopupMenuThemeData(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
color: scheme.surface,
elevation: 4,
),
); );
} }