diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index 86de8f42..830f5b87 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -98,7 +98,7 @@ class UserPlaylists extends HookConsumerWidget { Row( children: [ const SizedBox(width: 10), - const PlaylistCreateDialog(), + const PlaylistCreateDialogButton(), const SizedBox(width: 10), ElevatedButton.icon( icon: const Icon(SpotubeIcons.magic), diff --git a/lib/components/playlist/playlist_create_dialog.dart b/lib/components/playlist/playlist_create_dialog.dart index c6fc8dd5..f5d32186 100644 --- a/lib/components/playlist/playlist_create_dialog.dart +++ b/lib/components/playlist/playlist_create_dialog.dart @@ -10,92 +10,109 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/spotify_provider.dart'; class PlaylistCreateDialog extends HookConsumerWidget { - const PlaylistCreateDialog({Key? key}) : super(key: key); + /// Track ids to add to the playlist + final List trackIds; + const PlaylistCreateDialog({ + Key? key, + this.trackIds = const [], + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final spotify = ref.watch(spotifyProvider); + final playlistName = useTextEditingController(); + final description = useTextEditingController(); + final public = useState(false); + final collaborative = useState(false); + final client = useQueryClient(); + final navigator = Navigator.of(context); + + Future onCreate() async { + if (playlistName.text.isEmpty) return; + final me = await spotify.me.get(); + final playlist = await spotify.playlists.createPlaylist( + me.id!, + playlistName.text, + collaborative: collaborative.value, + public: public.value, + description: description.text, + ); + if (trackIds.isNotEmpty) { + await spotify.playlists.addTracks( + trackIds.map((id) => "spotify:track:$id").toList(), + playlist.id!, + ); + } + await client + .getQuery( + "current-user-playlists", + ) + ?.refresh(); + navigator.pop(playlist); + } + + return AlertDialog( + title: Text(context.l10n.create_a_playlist), + actions: [ + OutlinedButton( + child: Text(context.l10n.cancel), + onPressed: () { + Navigator.pop(context); + }, + ), + FilledButton( + onPressed: onCreate, + child: Text(context.l10n.create), + ), + ], + content: Container( + width: MediaQuery.of(context).size.width, + constraints: const BoxConstraints(maxWidth: 500), + child: ListView( + shrinkWrap: true, + children: [ + TextField( + controller: playlistName, + decoration: InputDecoration( + hintText: context.l10n.name_of_playlist, + labelText: context.l10n.name_of_playlist, + ), + ), + const SizedBox(height: 10), + TextField( + controller: description, + decoration: InputDecoration( + hintText: context.l10n.description, + ), + keyboardType: TextInputType.multiline, + maxLines: 5, + ), + const SizedBox(height: 10), + CheckboxListTile( + title: Text(context.l10n.public), + value: public.value, + onChanged: (val) => public.value = val ?? false, + ), + const SizedBox(height: 10), + CheckboxListTile( + title: Text(context.l10n.collaborative), + value: collaborative.value, + onChanged: (val) => collaborative.value = val ?? false, + ), + ], + ), + ), + ); + } +} + +class PlaylistCreateDialogButton extends HookConsumerWidget { + const PlaylistCreateDialogButton({Key? key}) : super(key: key); showPlaylistDialog(BuildContext context, SpotifyApi spotify) { showDialog( context: context, - builder: (context) { - return HookBuilder(builder: (context) { - final playlistName = useTextEditingController(); - final description = useTextEditingController(); - final public = useState(false); - final collaborative = useState(false); - final client = useQueryClient(); - final navigator = Navigator.of(context); - - onCreate() 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, - ); - await client - .getQuery( - "current-user-playlists", - ) - ?.refresh(); - navigator.pop(); - } - - return AlertDialog( - title: Text(context.l10n.create_a_playlist), - actions: [ - OutlinedButton( - child: Text(context.l10n.cancel), - onPressed: () { - Navigator.pop(context); - }, - ), - FilledButton( - onPressed: onCreate, - child: Text(context.l10n.create), - ), - ], - content: Container( - width: MediaQuery.of(context).size.width, - constraints: const BoxConstraints(maxWidth: 500), - child: ListView( - shrinkWrap: true, - children: [ - TextField( - controller: playlistName, - decoration: InputDecoration( - hintText: context.l10n.name_of_playlist, - labelText: context.l10n.name_of_playlist, - ), - ), - const SizedBox(height: 10), - TextField( - controller: description, - decoration: InputDecoration( - hintText: context.l10n.description, - ), - keyboardType: TextInputType.multiline, - maxLines: 5, - ), - const SizedBox(height: 10), - CheckboxListTile( - title: Text(context.l10n.public), - value: public.value, - onChanged: (val) => public.value = val ?? false, - ), - const SizedBox(height: 10), - CheckboxListTile( - title: Text(context.l10n.collaborative), - value: collaborative.value, - onChanged: (val) => collaborative.value = val ?? false, - ), - ], - ), - ), - ); - }); - }, + builder: (context) => const PlaylistCreateDialog(), ); } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 70bd18db..98298102 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -217,5 +217,9 @@ "min": "Min", "max": "Max", "target": "Target", - "moderate": "Moderate" + "moderate": "Moderate", + "deselect_all": "Deselect All", + "select_all": "Select All", + "generating_playlist": "Generating your custom playlist...", + "selected_count_tracks": "Selected {count} tracks" } \ No newline at end of file diff --git a/lib/pages/library/playlist_generate/playlist_generate.dart b/lib/pages/library/playlist_generate/playlist_generate.dart index c183dce5..ce2c64cb 100644 --- a/lib/pages/library/playlist_generate/playlist_generate.dart +++ b/lib/pages/library/playlist_generate/playlist_generate.dart @@ -454,37 +454,41 @@ class PlaylistGeneratorPage extends HookConsumerWidget { FilledButton.icon( icon: const Icon(SpotubeIcons.magic), label: Text(context.l10n.generate_playlist), - onPressed: () { - final PlaylistGenerateResultRouteState routeState = ( - seeds: ( - artists: artists.value.map((a) => a.id!).toList(), - tracks: tracks.value.map((t) => t.id!).toList(), - genres: genres.value - ), - market: market.value, - limit: limit.value, - parameters: ( - acousticness: acousticness.value, - danceability: danceability.value, - energy: energy.value, - instrumentalness: instrumentalness.value, - liveness: liveness.value, - loudness: loudness.value, - speechiness: speechiness.value, - valence: valence.value, - popularity: popularity.value, - key: key.value, - duration_ms: durationMs.value, - tempo: tempo.value, - mode: mode.value, - time_signature: timeSignature.value, - ) - ); - GoRouter.of(context).push( - "/library/generate/result", - extra: routeState, - ); - }, + onPressed: artists.value.isEmpty && + tracks.value.isEmpty && + genres.value.isEmpty + ? null + : () { + final PlaylistGenerateResultRouteState routeState = ( + seeds: ( + artists: artists.value.map((a) => a.id!).toList(), + tracks: tracks.value.map((t) => t.id!).toList(), + genres: genres.value + ), + market: market.value, + limit: limit.value, + parameters: ( + acousticness: acousticness.value, + danceability: danceability.value, + energy: energy.value, + instrumentalness: instrumentalness.value, + liveness: liveness.value, + loudness: loudness.value, + speechiness: speechiness.value, + valence: valence.value, + popularity: popularity.value, + key: key.value, + duration_ms: durationMs.value, + tempo: tempo.value, + mode: mode.value, + time_signature: timeSignature.value, + ) + ); + GoRouter.of(context).push( + "/library/generate/result", + extra: routeState, + ); + }, ), ], ); diff --git a/lib/pages/library/playlist_generate/playlist_generate_result.dart b/lib/pages/library/playlist_generate/playlist_generate_result.dart index 1a21c67a..973f4e61 100644 --- a/lib/pages/library/playlist_generate/playlist_generate_result.dart +++ b/lib/pages/library/playlist_generate/playlist_generate_result.dart @@ -1,9 +1,12 @@ import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/library/playlist_generate/simple_track_tile.dart'; +import 'package:spotube/components/playlist/playlist_create_dialog.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; @@ -27,6 +30,7 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final router = GoRouter.of(context); final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); final (:seeds, :parameters, :limit, :market) = state; @@ -62,13 +66,13 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { child: Scaffold( appBar: const PageWindowTitleBar(leading: BackButton()), body: generatedPlaylist.isLoading - ? const Center( + ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - CircularProgressIndicator(), - Text("Generating your custom playlist..."), + const CircularProgressIndicator(), + Text(context.l10n.generating_playlist), ], ), ) @@ -128,8 +132,23 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { FilledButton.tonalIcon( icon: const Icon(SpotubeIcons.addFilled), label: Text(context.l10n.create_a_playlist), - onPressed: - selectedTracks.value.isEmpty ? null : () {}, + onPressed: selectedTracks.value.isEmpty + ? null + : () async { + final playlist = await showDialog( + context: context, + builder: (context) => PlaylistCreateDialog( + trackIds: selectedTracks.value, + ), + ); + + if (playlist != null) { + router.go( + '/playlist/${playlist.id}', + extra: playlist, + ); + } + }, ), FilledButton.tonalIcon( icon: const Icon(SpotubeIcons.playlistAdd), @@ -141,24 +160,33 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { ), const SizedBox(height: 16), if (generatedPlaylist.data != null) - Align( - alignment: Alignment.centerRight, - child: ElevatedButton.icon( - onPressed: () { - if (isAllTrackSelected) { - selectedTracks.value = []; - } else { - selectedTracks.value = generatedPlaylist.data - ?.map((e) => e.id!) - .toList() ?? - []; - } - }, - icon: const Icon(SpotubeIcons.selectionCheck), - label: Text( - isAllTrackSelected ? "Deselect all" : "Select all", + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + context.l10n.selected_count_tracks( + selectedTracks.value.length, + ), ), - ), + ElevatedButton.icon( + onPressed: () { + if (isAllTrackSelected) { + selectedTracks.value = []; + } else { + selectedTracks.value = generatedPlaylist.data + ?.map((e) => e.id!) + .toList() ?? + []; + } + }, + icon: const Icon(SpotubeIcons.selectionCheck), + label: Text( + isAllTrackSelected + ? context.l10n.deselect_all + : context.l10n.select_all, + ), + ), + ], ), const SizedBox(height: 8), Card(