diff --git a/lib/components/Library/UserPlaylists.dart b/lib/components/Library/UserPlaylists.dart index ce6076c0..d8db87a9 100644 --- a/lib/components/Library/UserPlaylists.dart +++ b/lib/components/Library/UserPlaylists.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart' hide Image; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart'; +import 'package:spotube/components/Playlist/PlaylistCreateDialog.dart'; import 'package:spotube/provider/SpotifyDI.dart'; class UserPlaylists extends ConsumerWidget { @@ -37,6 +38,7 @@ class UserPlaylists extends ConsumerWidget { runSpacing: 20, // gap between lines alignment: WrapAlignment.center, children: [ + const PlaylistCreateDialog(), PlaylistCard(likedTracksPlaylist), ...snapshot.data! .map((playlist) => PlaylistCard(playlist)) diff --git a/lib/components/Playlist/PlaylistCard.dart b/lib/components/Playlist/PlaylistCard.dart index 307253ca..c631cdd0 100644 --- a/lib/components/Playlist/PlaylistCard.dart +++ b/lib/components/Playlist/PlaylistCard.dart @@ -22,7 +22,7 @@ class PlaylistCard extends HookConsumerWidget { return PlaybuttonCard( margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()), title: playlist.name!, - imageUrl: playlist.images![0].url!, + imageUrl: imageToUrlString(playlist.images), isPlaying: isPlaylistPlaying, onTap: () { GoRouter.of(context).push( diff --git a/lib/components/Playlist/PlaylistCreateDialog.dart b/lib/components/Playlist/PlaylistCreateDialog.dart new file mode 100644 index 00000000..7fef9060 --- /dev/null +++ b/lib/components/Playlist/PlaylistCreateDialog.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/provider/SpotifyDI.dart'; + +class PlaylistCreateDialog extends HookConsumerWidget { + const PlaylistCreateDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final spotify = ref.watch(spotifyProvider); + + return TextButton( + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.add_box_rounded, size: 50), + Text("Create Playlist", style: TextStyle(fontSize: 22)), + ], + ), + onPressed: () { + showDialog( + context: context, + builder: (context) { + return HookBuilder(builder: (context) { + final playlistName = useTextEditingController(); + final description = useTextEditingController(); + final public = useState(false); + final collaborative = useState(false); + + return AlertDialog( + title: const Text("Create a Playlist"), + actions: [ + TextButton( + child: const Text("Cancel"), + onPressed: () => Navigator.of(context).pop(), + ), + ElevatedButton( + child: const Text("Create"), + onPressed: () async { + if (playlistName.text.isEmpty) return; + final me = await spotify.me.get(); + await spotify.playlists + .createPlaylist( + me.id!, + playlistName.text, + collaborative: collaborative.value, + public: public.value, + description: description.text, + ) + .then((_) => Navigator.pop(context)); + }, + ) + ], + content: Container( + width: MediaQuery.of(context).size.width, + constraints: const BoxConstraints(maxWidth: 500), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: playlistName, + decoration: const InputDecoration( + hintText: "Name of the playlist", + label: Text("Playlist Name"), + ), + ), + const SizedBox(height: 10), + TextField( + controller: description, + keyboardType: TextInputType.multiline, + maxLines: 5, + decoration: const InputDecoration( + hintText: "Description...", + ), + ), + const SizedBox(height: 10), + CheckboxListTile( + value: public.value, + title: const Text("Public"), + onChanged: (val) => public.value = val ?? false, + ), + const SizedBox(height: 10), + CheckboxListTile( + value: collaborative.value, + title: const Text("Collaborative"), + onChanged: (val) => collaborative.value = val ?? false, + ), + ], + ), + ), + ); + }); + }, + ); + }, + style: ButtonStyle( + padding: MaterialStateProperty.all( + const EdgeInsets.symmetric(horizontal: 15, vertical: 100)), + ), + ); + } +} diff --git a/lib/components/Shared/TracksTableView.dart b/lib/components/Shared/TracksTableView.dart index dcf7f036..4f4f31ad 100644 --- a/lib/components/Shared/TracksTableView.dart +++ b/lib/components/Shared/TracksTableView.dart @@ -3,14 +3,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/Album/AlbumView.dart'; import 'package:spotube/components/Shared/LinkText.dart'; -import 'package:spotube/components/Shared/SpotubePageRoute.dart'; import 'package:spotube/helpers/artists-to-clickable-artists.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; +import 'package:spotube/hooks/useForceUpdate.dart'; +import 'package:spotube/models/Logger.dart'; +import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Playback.dart'; +import 'package:spotube/provider/SpotifyDI.dart'; class TracksTableView extends HookConsumerWidget { final void Function(Track currentTrack)? onTrackPlayButtonPressed; @@ -70,7 +72,8 @@ class TracksTableView extends HookConsumerWidget { const SizedBox(width: 10), Text("Time", style: tableHeadStyle), const SizedBox(width: 10), - ] + ], + const SizedBox(width: 40), ], ), ...tracks.asMap().entries.map((track) { @@ -95,13 +98,14 @@ class TracksTableView extends HookConsumerWidget { } } -class TrackTile extends HookWidget { +class TrackTile extends HookConsumerWidget { final Playback playback; final MapEntry track; final String duration; final String? thumbnailUrl; final void Function(Track currentTrack)? onTrackPlayButtonPressed; - const TrackTile( + final logger = getLogger(TrackTile); + TrackTile( this.playback, { required this.track, required this.duration, @@ -111,8 +115,102 @@ class TrackTile extends HookWidget { }) : super(key: key); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { final breakpoint = useBreakpoints(); + final auth = ref.watch(authProvider); + final spotify = ref.watch(spotifyProvider); + final update = useForceUpdate(); + + final actionFavorite = useCallback((bool isLiked) async { + try { + isLiked + ? await spotify.tracks.me.removeOne(track.value.id!) + : await spotify.tracks.me.saveOne(track.value.id!); + } catch (e, stack) { + logger.e("FavoriteButton.onPressed", e, stack); + } finally { + update(); + } + }, [track.value.id, spotify]); + + actionPlaylist() async { + showDialog( + context: context, + builder: (context) { + return FutureBuilder>( + future: spotify.playlists.me.all().then((playlists) async { + final me = await spotify.me.get(); + return playlists.where((playlist) => + playlist.owner?.id != null && + playlist.owner!.id == me.id); + }), + builder: (context, snapshot) { + return HookBuilder(builder: (context) { + final playlistsCheck = useState({}); + return AlertDialog( + title: Text( + "Add `${track.value.name}` to following Playlists"), + titleTextStyle: + Theme.of(context).textTheme.bodyText1?.copyWith( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + actions: [ + TextButton( + child: const Text("Cancel"), + onPressed: () => Navigator.pop(context), + ), + ElevatedButton( + child: const Text("Add"), + onPressed: () async { + final selectedPlaylists = playlistsCheck + .value.entries + .where((entry) => entry.value) + .map((entry) => entry.key); + + await Future.wait( + selectedPlaylists.map( + (playlistId) => spotify.playlists + .addTrack(track.value.uri!, playlistId), + ), + ).then((_) => Navigator.pop(context)); + }, + ) + ], + content: SizedBox( + height: 300, + width: 300, + child: !snapshot.hasData + ? const Center( + child: CircularProgressIndicator.adaptive()) + : ListView.builder( + shrinkWrap: true, + itemCount: snapshot.data!.length, + itemBuilder: (context, index) { + final playlist = + snapshot.data!.elementAt(index); + return CheckboxListTile( + title: Text(playlist.name!), + controlAffinity: + ListTileControlAffinity.leading, + value: playlistsCheck.value[playlist.id] ?? + false, + onChanged: (val) { + playlistsCheck.value = { + ...playlistsCheck.value, + playlist.id!: val == true + }; + }, + ); + }, + ), + ), + ); + }); + }); + }); + } + return Row( children: [ SizedBox( @@ -188,8 +286,52 @@ class TrackTile extends HookWidget { if (!breakpoint.isSm) ...[ const SizedBox(width: 10), Text(duration), - const SizedBox(width: 10) ], + const SizedBox(width: 10), + if (auth.isLoggedIn) + FutureBuilder( + future: spotify.tracks.me.containsOne(track.value.id!), + builder: (context, snapshot) { + return PopupMenuButton( + icon: const Icon(Icons.more_horiz_rounded), + itemBuilder: (context) { + return [ + PopupMenuItem( + child: Row( + children: const [ + Icon(Icons.add_box_rounded), + SizedBox(width: 10), + Text("Add to Playlist"), + ], + ), + value: "playlist", + ), + PopupMenuItem( + child: Row( + children: [ + Icon(snapshot.data == true + ? Icons.favorite_rounded + : Icons.favorite_border_rounded), + const SizedBox(width: 10), + const Text("Favorite") + ], + ), + value: "favorite", + ) + ]; + }, + onSelected: (value) { + switch (value) { + case "favorite": + actionFavorite(snapshot.data == true); + break; + case "playlist": + actionPlaylist(); + break; + } + }, + ); + }) ], ); }