mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
feat(queue): reorder tracks support
This commit is contained in:
parent
c1d67153ce
commit
441b43bef6
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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,132 +303,127 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
Text(duration),
|
Text(duration),
|
||||||
],
|
],
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
PopupMenuButton(
|
if (!isLocal)
|
||||||
icon: const Icon(SpotubeIcons.moreHorizontal),
|
PopupMenuButton(
|
||||||
elevation: 4,
|
icon: const Icon(SpotubeIcons.moreHorizontal),
|
||||||
position: PopupMenuPosition.under,
|
position: PopupMenuPosition.under,
|
||||||
tooltip: "More options",
|
tooltip: "More options",
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
return [
|
return [
|
||||||
if (!playlistQueueNotifier.isTrackOnQueue(track.value))
|
if (!playlistQueueNotifier.isTrackOnQueue(track.value))
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
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"),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
child: const ListTile(
|
},
|
||||||
leading: Icon(SpotubeIcons.queueAdd),
|
child: const ListTile(
|
||||||
title: Text("Add to queue"),
|
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"),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
if (toggler.item3.hasData)
|
||||||
else
|
PopupMenuItem(
|
||||||
PopupMenuItem(
|
padding: EdgeInsets.zero,
|
||||||
padding: EdgeInsets.zero,
|
onTap: () {
|
||||||
onTap: () {
|
toggler.item2.mutate(toggler.item1);
|
||||||
playlistQueueNotifier.remove([track.value]);
|
},
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
child: ListTile(
|
||||||
SnackBar(
|
leading: toggler.item1
|
||||||
content:
|
? const Icon(
|
||||||
Text("Removed ${track.value.name} from queue"),
|
SpotubeIcons.heartFilled,
|
||||||
),
|
color: Colors.pink,
|
||||||
);
|
)
|
||||||
},
|
: const Icon(SpotubeIcons.heart),
|
||||||
child: const ListTile(
|
title: const Text("Save as favorite"),
|
||||||
leading: Icon(SpotubeIcons.queueRemove),
|
),
|
||||||
title: Text("Remove from queue"),
|
),
|
||||||
|
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(
|
PopupMenuItem(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
onTap: () {
|
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(
|
child: ListTile(
|
||||||
leading: toggler.item1
|
leading: const Icon(SpotubeIcons.playlistRemove),
|
||||||
? const Icon(
|
iconColor: !isBlackListed ? Colors.red[400] : null,
|
||||||
SpotubeIcons.heartFilled,
|
textColor: !isBlackListed ? Colors.red[400] : null,
|
||||||
color: Colors.pink,
|
title: Text(
|
||||||
)
|
"${isBlackListed ? "Remove from" : "Add to"} blacklist",
|
||||||
: 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"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
PopupMenuItem(
|
||||||
];
|
padding: EdgeInsets.zero,
|
||||||
},
|
onTap: () {
|
||||||
),
|
actionShare(track.value);
|
||||||
|
},
|
||||||
|
child: const ListTile(
|
||||||
|
leading: Icon(SpotubeIcons.share),
|
||||||
|
title: Text("Share"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
},
|
||||||
|
),
|
||||||
...?actions,
|
...?actions,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user