Skip to content

Commit

Permalink
feat(queue): reorder tracks support
Browse files Browse the repository at this point in the history
  • Loading branch information
Kingkor Roy Tirtho committed Apr 28, 2023
1 parent c1d6715 commit 441b43b
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 114 deletions.
1 change: 1 addition & 0 deletions lib/collections/spotube_icons.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
14 changes: 12 additions & 2 deletions lib/components/player/player_queue.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -135,6 +139,12 @@ class PlayerQueue extends HookConsumerWidget {
}
await playlistNotifier.playTrack(currentTrack);
},
leadingActions: [
ReorderableDragStartListener(
index: i,
child: const Icon(SpotubeIcons.dragHandle),
),
],
),
),
);
Expand Down
222 changes: 110 additions & 112 deletions lib/components/shared/track_table/track_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class TrackTile extends HookConsumerWidget {
final void Function(bool?)? onCheckChange;

final List<Widget>? actions;
final List<Widget>? leadingActions;

TrackTile(
this.playlist, {
Expand All @@ -56,6 +57,7 @@ class TrackTile extends HookConsumerWidget {
this.isLocal = false,
this.onCheckChange,
this.actions,
this.leadingActions,
Key? key,
}) : super(key: key);

Expand Down Expand Up @@ -190,6 +192,7 @@ class TrackTile extends HookConsumerWidget {
type: MaterialType.transparency,
child: Row(
children: [
...?leadingActions,
if (showCheck && !isBlackListed)
Checkbox(
value: isChecked,
Expand Down Expand Up @@ -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,
],
),
Expand Down
12 changes: 12 additions & 0 deletions lib/provider/playlist_queue_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {

// skip all the activeTrack.skipSegments
if (state?.isLoading != true &&
state?.activeTrack is SpotubeTrack &&
(state?.activeTrack as SpotubeTrack?)?.skipSegments.isNotEmpty ==
true &&
preferences.skipSponsorSegments) {
Expand Down Expand Up @@ -508,6 +509,17 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
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<PlaylistQueue>? fromJson(Map<String, dynamic> json) {
if (json.isEmpty) return null;
Expand Down
5 changes: 5 additions & 0 deletions lib/themes/theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
);
}

0 comments on commit 441b43b

Please sign in to comment.