feat: add selected tracks to playlists, optimistic playlist remove track

This commit is contained in:
Kingkor Roy Tirtho 2022-12-07 23:15:28 +06:00
parent ee5c417ac3
commit 3386dac78e
6 changed files with 176 additions and 23 deletions

View File

@ -87,7 +87,12 @@ class PlaylistView extends HookConsumerWidget {
onPlay: ([track]) { onPlay: ([track]) {
if (tracksSnapshot.hasData) { if (tracksSnapshot.hasData) {
if (!isPlaylistPlaying) { if (!isPlaylistPlaying) {
playPlaylist(playback, tracksSnapshot.data!, ref); playPlaylist(
playback,
tracksSnapshot.data!,
ref,
currentTrack: track,
);
} else if (isPlaylistPlaying && track != null) { } else if (isPlaylistPlaying && track != null) {
playPlaylist( playPlaylist(
playback, playback,

View File

@ -6,7 +6,7 @@ import 'package:spotube/hooks/useBreakpoints.dart';
class Action extends StatelessWidget { class Action extends StatelessWidget {
final Widget text; final Widget text;
final Icon icon; final Widget icon;
final void Function() onPressed; final void Function() onPressed;
final bool isExpanded; final bool isExpanded;
const Action({ const Action({

View File

@ -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
};
},
);
},
),
),
);
}
}

View File

@ -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/material.dart' hide Action;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -14,6 +16,7 @@ import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -55,18 +58,21 @@ class TrackTile extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final isReallyLocal = isLocal ||
ref.watch(
playbackProvider.select((s) => s.playlist?.isLocal == true),
);
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final auth = ref.watch(authProvider); final auth = ref.watch(authProvider);
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final removingTrack = useState<String?>(null);
final actionRemoveFromPlaylist = useCallback(() async { final removeTrack = useMutation<bool, Tuple2<SpotifyApi, String>>(
if (playlistId == null) return; job: removeTrackFromPlaylistMutationJob(playlistId ?? ""),
return await spotify.playlists.removeTrack(track.value.uri!, playlistId!); onData: (payload, variables, ctx) {
}, [playlistId, spotify, track.value.uri]); if (playlistId == null || !payload) return;
QueryBowl.of(context)
.getQuery(
playlistTracksQueryJob(playlistId!).queryKey,
)
?.refetch();
},
);
void actionShare(Track track) { void actionShare(Track track) {
final data = "https://open.spotify.com/track/${track.id}"; final data = "https://open.spotify.com/track/${track.id}";
@ -244,7 +250,7 @@ class TrackTile extends HookConsumerWidget {
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
isReallyLocal isLocal
? PlatformText( ? PlatformText(
TypeConversionUtils.artists_X_String<Artist>( TypeConversionUtils.artists_X_String<Artist>(
track.value.artists ?? []), track.value.artists ?? []),
@ -260,7 +266,7 @@ class TrackTile extends HookConsumerWidget {
), ),
if (breakpoint.isMoreThan(Breakpoints.md) && showAlbum) if (breakpoint.isMoreThan(Breakpoints.md) && showAlbum)
Expanded( Expanded(
child: isReallyLocal child: isLocal
? PlatformText(track.value.album?.name ?? "") ? PlatformText(track.value.album?.name ?? "")
: LinkText( : LinkText(
track.value.album!.name!, track.value.album!.name!,
@ -274,7 +280,7 @@ class TrackTile extends HookConsumerWidget {
PlatformText(duration), PlatformText(duration),
], ],
const SizedBox(width: 10), const SizedBox(width: 10),
if (!isReallyLocal) if (!isLocal)
AdaptiveActions( AdaptiveActions(
actions: [ actions: [
if (toggler.item3.hasData) if (toggler.item3.hasData)
@ -298,9 +304,17 @@ class TrackTile extends HookConsumerWidget {
), ),
if (userPlaylist && auth.isLoggedIn) if (userPlaylist && auth.isLoggedIn)
Action( 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"), text: const PlatformText("Remove from playlist"),
onPressed: actionRemoveFromPlaylist, onPressed: () {
removingTrack.value = track.value.uri;
removeTrack.mutate(Tuple2(spotify, track.value.uri!));
},
), ),
Action( Action(
icon: const Icon(Icons.share_rounded), icon: const Icon(Icons.share_rounded),

View File

@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart'; import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Library/UserLocalTracks.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/DownloadConfirmationDialog.dart';
import 'package:spotube/components/Shared/NotFound.dart'; import 'package:spotube/components/Shared/NotFound.dart';
import 'package:spotube/components/Shared/SortTracksDropdown.dart'; import 'package:spotube/components/Shared/SortTracksDropdown.dart';
@ -123,14 +124,16 @@ class TracksTableView extends HookConsumerWidget {
value: sortBy, value: sortBy,
onChanged: (value) { onChanged: (value) {
ref ref
.read(trackCollectionSortState(playlistId ?? '').state) .read(
trackCollectionSortState(playlistId ?? '').notifier)
.state = value; .state = value;
}, },
), ),
PlatformPopupMenuButton( PlatformPopupMenuButton(
closeAfterClick: false,
items: [ items: [
PlatformPopupMenuItem( PlatformPopupMenuItem(
enabled: selected.value.isNotEmpty, enabled: selectedTracks.isNotEmpty,
value: "download", value: "download",
child: Row( child: Row(
children: [ children: [
@ -141,16 +144,31 @@ 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 { onSelected: (action) async {
switch (action) { switch (action) {
case "download": case "download":
{ {
final isConfirmed = await showPlatformAlertDialog( final confirmed = await showPlatformAlertDialog(
context, builder: (context) { context,
builder: (context) {
return const DownloadConfirmationDialog(); return const DownloadConfirmationDialog();
}); },
if (isConfirmed != true) return; );
if (confirmed != true) return;
for (final selectedTrack in selectedTracks) { for (final selectedTrack in selectedTracks) {
downloader.addToQueue(selectedTrack); downloader.addToQueue(selectedTrack);
} }
@ -158,11 +176,24 @@ class TracksTableView extends HookConsumerWidget {
showCheck.value = false; showCheck.value = false;
break; break;
} }
case "add-to-playlist":
{
await showPlatformAlertDialog(
context,
builder: (context) {
return AddTracksToPlaylistDialog(
tracks: selectedTracks.toList(),
);
},
);
break;
}
default: default:
} }
}, },
child: const Icon(Icons.more_vert), child: const Icon(Icons.more_vert),
), ),
const SizedBox(width: 10),
], ],
), ),
...sortedTracks.asMap().entries.map((track) { ...sortedTracks.asMap().entries.map((track) {

View File

@ -276,3 +276,16 @@ final toggleFavoriteAlbumMutationJob =
return !isLiked; 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;
},
);