mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
feat: playlist create support for generated playlist
This commit is contained in:
parent
51e427e83c
commit
91c72f9ec9
@ -98,7 +98,7 @@ class UserPlaylists extends HookConsumerWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
const PlaylistCreateDialog(),
|
const PlaylistCreateDialogButton(),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: const Icon(SpotubeIcons.magic),
|
icon: const Icon(SpotubeIcons.magic),
|
||||||
|
@ -10,92 +10,109 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
|
|
||||||
class PlaylistCreateDialog extends HookConsumerWidget {
|
class PlaylistCreateDialog extends HookConsumerWidget {
|
||||||
const PlaylistCreateDialog({Key? key}) : super(key: key);
|
/// Track ids to add to the playlist
|
||||||
|
final List<String> 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<void> 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) {
|
showPlaylistDialog(BuildContext context, SpotifyApi spotify) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) => const PlaylistCreateDialog(),
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,5 +217,9 @@
|
|||||||
"min": "Min",
|
"min": "Min",
|
||||||
"max": "Max",
|
"max": "Max",
|
||||||
"target": "Target",
|
"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"
|
||||||
}
|
}
|
@ -454,37 +454,41 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
icon: const Icon(SpotubeIcons.magic),
|
icon: const Icon(SpotubeIcons.magic),
|
||||||
label: Text(context.l10n.generate_playlist),
|
label: Text(context.l10n.generate_playlist),
|
||||||
onPressed: () {
|
onPressed: artists.value.isEmpty &&
|
||||||
final PlaylistGenerateResultRouteState routeState = (
|
tracks.value.isEmpty &&
|
||||||
seeds: (
|
genres.value.isEmpty
|
||||||
artists: artists.value.map((a) => a.id!).toList(),
|
? null
|
||||||
tracks: tracks.value.map((t) => t.id!).toList(),
|
: () {
|
||||||
genres: genres.value
|
final PlaylistGenerateResultRouteState routeState = (
|
||||||
),
|
seeds: (
|
||||||
market: market.value,
|
artists: artists.value.map((a) => a.id!).toList(),
|
||||||
limit: limit.value,
|
tracks: tracks.value.map((t) => t.id!).toList(),
|
||||||
parameters: (
|
genres: genres.value
|
||||||
acousticness: acousticness.value,
|
),
|
||||||
danceability: danceability.value,
|
market: market.value,
|
||||||
energy: energy.value,
|
limit: limit.value,
|
||||||
instrumentalness: instrumentalness.value,
|
parameters: (
|
||||||
liveness: liveness.value,
|
acousticness: acousticness.value,
|
||||||
loudness: loudness.value,
|
danceability: danceability.value,
|
||||||
speechiness: speechiness.value,
|
energy: energy.value,
|
||||||
valence: valence.value,
|
instrumentalness: instrumentalness.value,
|
||||||
popularity: popularity.value,
|
liveness: liveness.value,
|
||||||
key: key.value,
|
loudness: loudness.value,
|
||||||
duration_ms: durationMs.value,
|
speechiness: speechiness.value,
|
||||||
tempo: tempo.value,
|
valence: valence.value,
|
||||||
mode: mode.value,
|
popularity: popularity.value,
|
||||||
time_signature: timeSignature.value,
|
key: key.value,
|
||||||
)
|
duration_ms: durationMs.value,
|
||||||
);
|
tempo: tempo.value,
|
||||||
GoRouter.of(context).push(
|
mode: mode.value,
|
||||||
"/library/generate/result",
|
time_signature: timeSignature.value,
|
||||||
extra: routeState,
|
)
|
||||||
);
|
);
|
||||||
},
|
GoRouter.of(context).push(
|
||||||
|
"/library/generate/result",
|
||||||
|
extra: routeState,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/library/playlist_generate/simple_track_tile.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/components/shared/page_window_title_bar.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
@ -27,6 +30,7 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
|
final router = GoRouter.of(context);
|
||||||
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
final (:seeds, :parameters, :limit, :market) = state;
|
final (:seeds, :parameters, :limit, :market) = state;
|
||||||
|
|
||||||
@ -62,13 +66,13 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
|
|||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: const PageWindowTitleBar(leading: BackButton()),
|
appBar: const PageWindowTitleBar(leading: BackButton()),
|
||||||
body: generatedPlaylist.isLoading
|
body: generatedPlaylist.isLoading
|
||||||
? const Center(
|
? Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(),
|
const CircularProgressIndicator(),
|
||||||
Text("Generating your custom playlist..."),
|
Text(context.l10n.generating_playlist),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -128,8 +132,23 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
|
|||||||
FilledButton.tonalIcon(
|
FilledButton.tonalIcon(
|
||||||
icon: const Icon(SpotubeIcons.addFilled),
|
icon: const Icon(SpotubeIcons.addFilled),
|
||||||
label: Text(context.l10n.create_a_playlist),
|
label: Text(context.l10n.create_a_playlist),
|
||||||
onPressed:
|
onPressed: selectedTracks.value.isEmpty
|
||||||
selectedTracks.value.isEmpty ? null : () {},
|
? null
|
||||||
|
: () async {
|
||||||
|
final playlist = await showDialog<Playlist>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PlaylistCreateDialog(
|
||||||
|
trackIds: selectedTracks.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (playlist != null) {
|
||||||
|
router.go(
|
||||||
|
'/playlist/${playlist.id}',
|
||||||
|
extra: playlist,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
FilledButton.tonalIcon(
|
FilledButton.tonalIcon(
|
||||||
icon: const Icon(SpotubeIcons.playlistAdd),
|
icon: const Icon(SpotubeIcons.playlistAdd),
|
||||||
@ -141,24 +160,33 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (generatedPlaylist.data != null)
|
if (generatedPlaylist.data != null)
|
||||||
Align(
|
Row(
|
||||||
alignment: Alignment.centerRight,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
child: ElevatedButton.icon(
|
children: [
|
||||||
onPressed: () {
|
Text(
|
||||||
if (isAllTrackSelected) {
|
context.l10n.selected_count_tracks(
|
||||||
selectedTracks.value = [];
|
selectedTracks.value.length,
|
||||||
} else {
|
),
|
||||||
selectedTracks.value = generatedPlaylist.data
|
|
||||||
?.map((e) => e.id!)
|
|
||||||
.toList() ??
|
|
||||||
[];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(SpotubeIcons.selectionCheck),
|
|
||||||
label: Text(
|
|
||||||
isAllTrackSelected ? "Deselect all" : "Select all",
|
|
||||||
),
|
),
|
||||||
),
|
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),
|
const SizedBox(height: 8),
|
||||||
Card(
|
Card(
|
||||||
|
Loading…
Reference in New Issue
Block a user