import 'dart:convert'; import 'dart:io'; import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path/path.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/form/checkbox_form_field.dart'; import 'package:spotube/components/form/text_form_field.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify_provider.dart'; class PlaylistCreateDialog extends HookConsumerWidget { /// Track ids to add to the playlist final List trackIds; final String? playlistId; const PlaylistCreateDialog({ super.key, this.trackIds = const [], this.playlistId, }); @override Widget build(BuildContext context, ref) { final userPlaylists = ref.watch(favoritePlaylistsProvider); final playlist = ref.watch(playlistProvider(playlistId ?? "")); final playlistNotifier = ref.watch(playlistProvider(playlistId ?? "").notifier); final isSubmitting = useState(false); final formKey = useMemoized(() => GlobalKey(), []); final updatingPlaylist = useMemoized( () => userPlaylists.asData?.value.items .firstWhereOrNull((playlist) => playlist.id == playlistId), [ userPlaylists.asData?.value.items, playlistId, ], ); final isUpdatingPlaylist = playlistId != null; final l10n = context.l10n; final theme = Theme.of(context); final onError = useCallback((error) { if (error is SpotifyError || error is SpotifyException) { showToast( context: context, location: ToastLocation.topRight, builder: (context, overlay) { return SurfaceCard( child: Basic( title: Text( l10n.error(error.message ?? l10n.epic_failure), style: theme.typography.normal.copyWith( color: theme.colorScheme.destructive, ), ), ), ); }, ); } }, [l10n, theme]); Future onCreate() async { if (!formKey.currentState!.saveAndValidate()) return; try { isSubmitting.value = true; final values = formKey.currentState!.value; final PlaylistInput payload = ( playlistName: values['playlistName'], collaborative: values['collaborative'], public: values['public'], description: values['description'], base64Image: (values['image'] as XFile?)?.path != null ? await (values['image'] as XFile) .readAsBytes() .then((bytes) => base64Encode(bytes)) : null, ); if (isUpdatingPlaylist) { await playlistNotifier.modify(payload, onError); } else { await playlistNotifier.create(payload, onError); } if (trackIds.isNotEmpty) { await playlistNotifier.addTracks(trackIds, onError); } } finally { isSubmitting.value = false; if (context.mounted && !ref.read(playlistProvider(playlistId ?? "")).hasError) { context.router.maybePop( await ref.read(playlistProvider(playlistId ?? "").future), ); } } } return AlertDialog( title: Text( isUpdatingPlaylist ? context.l10n.update_playlist : context.l10n.create_a_playlist, ), actions: [ Button.outline( child: Text(context.l10n.cancel), onPressed: () { Navigator.pop(context); }, ), Button.primary( onPressed: onCreate, enabled: !playlist.isLoading & !isSubmitting.value, child: Text( isUpdatingPlaylist ? context.l10n.update : context.l10n.create, ), ), ], content: Container( width: MediaQuery.of(context).size.width, constraints: const BoxConstraints(maxWidth: 500), child: FormBuilder( key: formKey, initialValue: { 'playlistName': updatingPlaylist?.name, 'description': updatingPlaylist?.description, 'public': updatingPlaylist?.public ?? false, 'collaborative': updatingPlaylist?.collaborative ?? false, }, child: ListView( shrinkWrap: true, children: [ FormBuilderField( name: 'image', validator: (value) { if (value == null) return null; final file = File(value.path); if (file.lengthSync() > 256000) { return "Image size should be less than 256kb"; } if (extension(file.path) != ".png") { return "Image should be in PNG format"; } return null; }, builder: (field) { return Column( spacing: 10, children: [ UniversalImage( path: field.value?.path ?? (updatingPlaylist?.images).asUrlString( placeholder: ImagePlaceholder.collection, ), height: 200, ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Button.secondary( leading: const Icon(SpotubeIcons.edit), child: Text( field.value?.path != null || updatingPlaylist?.images != null ? context.l10n.change_cover : context.l10n.add_cover, ), onPressed: () async { final imageFile = await ImagePicker().pickImage( source: ImageSource.gallery, ); if (imageFile != null) { field.didChange(imageFile); field.validate(); field.save(); } }, ), const SizedBox(width: 10), IconButton.destructive( icon: const Icon(SpotubeIcons.trash), enabled: field.value != null, onPressed: () { field.didChange(null); field.validate(); field.save(); }, ), ], ), if (field.hasError) Text( field.errorText ?? "", style: theme.typography.normal.copyWith( color: theme.colorScheme.destructive, ), ) ], ); }, ), const Gap(20), TextFormBuilderField( name: 'playlistName', label: Text(context.l10n.playlist_name), placeholder: Text(context.l10n.name_of_playlist), validator: FormBuilderValidators.required(), ), const Gap(20), TextFormBuilderField( name: 'description', label: Text(context.l10n.description), validator: FormBuilderValidators.required(), placeholder: Text(context.l10n.description), keyboardType: TextInputType.multiline, maxLines: 5, ), const Gap(20), CheckboxFormBuilderField( name: 'public', trailing: Text(context.l10n.public), ), const Gap(10), CheckboxFormBuilderField( name: 'collaborative', trailing: Text(context.l10n.collaborative), ), ], ), ), ), ); } } class PlaylistCreateDialogButton extends HookConsumerWidget { const PlaylistCreateDialogButton({super.key}); showPlaylistDialog(BuildContext context, SpotifyApi spotify) { showDialog( context: context, alignment: Alignment.center, builder: (context) => const ToastLayer( child: PlaylistCreateDialog(), ), ); } @override Widget build(BuildContext context, ref) { final spotify = ref.watch(spotifyProvider); return Button.secondary( leading: const Icon(SpotubeIcons.addFilled), child: Text(context.l10n.playlist), onPressed: () => showPlaylistDialog(context, spotify), ); } }