mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
feat: add selected tracks to playlists, optimistic playlist remove track
This commit is contained in:
parent
ee5c417ac3
commit
3386dac78e
@ -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,
|
||||
|
@ -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({
|
||||
|
90
lib/components/Shared/AddTracksToPlaylistDialog.dart
Normal file
90
lib/components/Shared/AddTracksToPlaylistDialog.dart
Normal 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
|
||||
};
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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';
|
||||
@ -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';
|
||||
|
||||
@ -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}";
|
||||
@ -244,7 +250,7 @@ class TrackTile extends HookConsumerWidget {
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
isReallyLocal
|
||||
isLocal
|
||||
? PlatformText(
|
||||
TypeConversionUtils.artists_X_String<Artist>(
|
||||
track.value.artists ?? []),
|
||||
@ -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!,
|
||||
@ -274,7 +280,7 @@ class TrackTile extends HookConsumerWidget {
|
||||
PlatformText(duration),
|
||||
],
|
||||
const SizedBox(width: 10),
|
||||
if (!isReallyLocal)
|
||||
if (!isLocal)
|
||||
AdaptiveActions(
|
||||
actions: [
|
||||
if (toggler.item3.hasData)
|
||||
@ -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),
|
||||
|
@ -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';
|
||||
@ -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: [
|
||||
@ -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 {
|
||||
switch (action) {
|
||||
case "download":
|
||||
{
|
||||
final isConfirmed = await showPlatformAlertDialog(
|
||||
context, builder: (context) {
|
||||
final confirmed = await showPlatformAlertDialog(
|
||||
context,
|
||||
builder: (context) {
|
||||
return const DownloadConfirmationDialog();
|
||||
});
|
||||
if (isConfirmed != true) return;
|
||||
},
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
for (final selectedTrack in selectedTracks) {
|
||||
downloader.addToQueue(selectedTrack);
|
||||
}
|
||||
@ -158,11 +176,24 @@ class TracksTableView extends HookConsumerWidget {
|
||||
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) {
|
||||
|
@ -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;
|
||||
},
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user