Skip to content

Commit

Permalink
feat: add selected tracks to playlists, optimistic playlist remove track
Browse files Browse the repository at this point in the history
  • Loading branch information
KRTirtho committed Dec 7, 2022
1 parent ee5c417 commit 3386dac
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 23 deletions.
7 changes: 6 additions & 1 deletion lib/components/Playlist/PlaylistView.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@ class PlaylistView extends HookConsumerWidget {
onPlay: ([track]) {
if (tracksSnapshot.hasData) {
if (!isPlaylistPlaying) {
playPlaylist(playback, tracksSnapshot.data!, ref);
playPlaylist(
playback,
tracksSnapshot.data!,
ref,
currentTrack: track,
);
} else if (isPlaylistPlaying && track != null) {
playPlaylist(
playback,
Expand Down
2 changes: 1 addition & 1 deletion lib/components/Shared/AdaptivePopupMenuButton.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:spotube/hooks/useBreakpoints.dart';

class Action extends StatelessWidget {
final Widget text;
final Icon icon;
final Widget icon;
final void Function() onPressed;
final bool isExpanded;
const Action({
Expand Down
90 changes: 90 additions & 0 deletions lib/components/Shared/AddTracksToPlaylistDialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart';

class AddTracksToPlaylistDialog extends HookConsumerWidget {
final List<Track> tracks;
const AddTracksToPlaylistDialog({
required this.tracks,
Key? key,
}) : super(key: key);

@override
Widget build(BuildContext context, ref) {
final spotify = ref.watch(spotifyProvider);
final userPlaylists = useQuery(
job: currentUserPlaylistsQueryJob,
externalData: spotify,
);
final me = useQuery(
job: currentUserQueryJob,
externalData: spotify,
);
final filteredPlaylists = userPlaylists.data?.where(
(playlist) =>
playlist.owner?.id != null && playlist.owner!.id == me.data?.id,
);
final playlistsCheck = useState(<String, bool>{});

return PlatformAlertDialog(
title: const PlatformText("Add to Playlist"),
secondaryActions: [
PlatformFilledButton(
isSecondary: true,
child: const PlatformText("Cancel"),
onPressed: () => Navigator.pop(context),
),
],
primaryActions: [
PlatformFilledButton(
child: const PlatformText("Add"),
onPressed: () async {
final selectedPlaylists = playlistsCheck.value.entries
.where((entry) => entry.value)
.map((entry) => entry.key);

await Future.wait(
selectedPlaylists.map(
(playlistId) => spotify.playlists.addTracks(
tracks
.map(
(track) => track.uri!,
)
.toList(),
playlistId),
),
).then((_) => Navigator.pop(context));
},
)
],
content: SizedBox(
height: 300,
width: 300,
child: !userPlaylists.hasData
? const Center(child: PlatformCircularProgressIndicator())
: ListView.builder(
shrinkWrap: true,
itemCount: filteredPlaylists!.length,
itemBuilder: (context, index) {
final playlist = filteredPlaylists.elementAt(index);
return PlatformCheckbox(
label: PlatformText(playlist.name!),
value: playlistsCheck.value[playlist.id] ?? false,
onChanged: (val) {
playlistsCheck.value = {
...playlistsCheck.value,
playlist.id!: val == true
};
},
);
},
),
),
);
}
}
42 changes: 28 additions & 14 deletions lib/components/Shared/TrackTile.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:fl_query/fl_query.dart';
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart' hide Action;
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
Expand All @@ -14,6 +16,7 @@ import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:tuple/tuple.dart';

Expand Down Expand Up @@ -55,18 +58,21 @@ class TrackTile extends HookConsumerWidget {

@override
Widget build(BuildContext context, ref) {
final isReallyLocal = isLocal ||
ref.watch(
playbackProvider.select((s) => s.playlist?.isLocal == true),
);
final breakpoint = useBreakpoints();
final auth = ref.watch(authProvider);
final spotify = ref.watch(spotifyProvider);

final actionRemoveFromPlaylist = useCallback(() async {
if (playlistId == null) return;
return await spotify.playlists.removeTrack(track.value.uri!, playlistId!);
}, [playlistId, spotify, track.value.uri]);
final removingTrack = useState<String?>(null);
final removeTrack = useMutation<bool, Tuple2<SpotifyApi, String>>(
job: removeTrackFromPlaylistMutationJob(playlistId ?? ""),
onData: (payload, variables, ctx) {
if (playlistId == null || !payload) return;
QueryBowl.of(context)
.getQuery(
playlistTracksQueryJob(playlistId!).queryKey,
)
?.refetch();
},
);

void actionShare(Track track) {
final data = "https://open.spotify.com/track/${track.id}";
Expand Down Expand Up @@ -244,7 +250,7 @@ class TrackTile extends HookConsumerWidget {
),
overflow: TextOverflow.ellipsis,
),
isReallyLocal
isLocal
? PlatformText(
TypeConversionUtils.artists_X_String<Artist>(
track.value.artists ?? []),
Expand All @@ -260,7 +266,7 @@ class TrackTile extends HookConsumerWidget {
),
if (breakpoint.isMoreThan(Breakpoints.md) && showAlbum)
Expanded(
child: isReallyLocal
child: isLocal
? PlatformText(track.value.album?.name ?? "")
: LinkText(
track.value.album!.name!,
Expand All @@ -274,7 +280,7 @@ class TrackTile extends HookConsumerWidget {
PlatformText(duration),
],
const SizedBox(width: 10),
if (!isReallyLocal)
if (!isLocal)
AdaptiveActions(
actions: [
if (toggler.item3.hasData)
Expand All @@ -298,9 +304,17 @@ class TrackTile extends HookConsumerWidget {
),
if (userPlaylist && auth.isLoggedIn)
Action(
icon: const Icon(Icons.remove_circle_outline_rounded),
icon: removeTrack.isLoading &&
removingTrack.value == track.value.uri
? const Center(
child: PlatformCircularProgressIndicator(),
)
: const Icon(Icons.remove_circle_outline_rounded),
text: const PlatformText("Remove from playlist"),
onPressed: actionRemoveFromPlaylist,
onPressed: () {
removingTrack.value = track.value.uri;
removeTrack.mutate(Tuple2(spotify, track.value.uri!));
},
),
Action(
icon: const Icon(Icons.share_rounded),
Expand Down
45 changes: 38 additions & 7 deletions lib/components/Shared/TracksTableView.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Library/UserLocalTracks.dart';
import 'package:spotube/components/Shared/AddTracksToPlaylistDialog.dart';
import 'package:spotube/components/Shared/DownloadConfirmationDialog.dart';
import 'package:spotube/components/Shared/NotFound.dart';
import 'package:spotube/components/Shared/SortTracksDropdown.dart';
Expand Down Expand Up @@ -123,14 +124,16 @@ class TracksTableView extends HookConsumerWidget {
value: sortBy,
onChanged: (value) {
ref
.read(trackCollectionSortState(playlistId ?? '').state)
.read(
trackCollectionSortState(playlistId ?? '').notifier)
.state = value;
},
),
PlatformPopupMenuButton(
closeAfterClick: false,
items: [
PlatformPopupMenuItem(
enabled: selected.value.isNotEmpty,
enabled: selectedTracks.isNotEmpty,
value: "download",
child: Row(
children: [
Expand All @@ -141,28 +144,56 @@ class TracksTableView extends HookConsumerWidget {
],
),
),
if (!userPlaylist)
PlatformPopupMenuItem(
enabled: selectedTracks.isNotEmpty,
value: "add-to-playlist",
child: Row(
children: [
const Icon(Icons.playlist_add_rounded),
PlatformText(
"Add (${selectedTracks.length}) to Playlist",
),
],
),
),
],
onSelected: (action) async {
switch (action) {
case "download":
{
final isConfirmed = await showPlatformAlertDialog(
context, builder: (context) {
return const DownloadConfirmationDialog();
});
if (isConfirmed != true) return;
final confirmed = await showPlatformAlertDialog(
context,
builder: (context) {
return const DownloadConfirmationDialog();
},
);
if (confirmed != true) return;
for (final selectedTrack in selectedTracks) {
downloader.addToQueue(selectedTrack);
}
selected.value = [];
showCheck.value = false;
break;
}
case "add-to-playlist":
{
await showPlatformAlertDialog(
context,
builder: (context) {
return AddTracksToPlaylistDialog(
tracks: selectedTracks.toList(),
);
},
);
break;
}
default:
}
},
child: const Icon(Icons.more_vert),
),
const SizedBox(width: 10),
],
),
...sortedTracks.asMap().entries.map((track) {
Expand Down
13 changes: 13 additions & 0 deletions lib/provider/SpotifyRequests.dart
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,16 @@ final toggleFavoriteAlbumMutationJob =
return !isLiked;
},
);

final removeTrackFromPlaylistMutationJob =
MutationJob.withVariableKey<bool, Tuple2<SpotifyApi, String>>(
preMutationKey: "remove-track-from-playlist",
task: (queryKey, externalData) async {
final spotify = externalData.item1;
final playlistId = getVariable(queryKey);
final trackId = externalData.item2;

await spotify.playlists.removeTracks([trackId], playlistId);
return true;
},
);

0 comments on commit 3386dac

Please sign in to comment.