mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
287 lines
9.7 KiB
Dart
287 lines
9.7 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
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:go_router/go_router.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/constrains.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<String> 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<FormBuilderState>(), []);
|
|
|
|
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<void> 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);
|
|
}
|
|
} finally {
|
|
isSubmitting.value = false;
|
|
if (context.mounted &&
|
|
!ref.read(playlistProvider(playlistId ?? "")).hasError) {
|
|
context.pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
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<XFile?>(
|
|
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 mediaQuery = MediaQuery.of(context);
|
|
final spotify = ref.watch(spotifyProvider);
|
|
|
|
if (mediaQuery.smAndDown) {
|
|
return IconButton.secondary(
|
|
icon: const Icon(SpotubeIcons.addFilled),
|
|
onPressed: () => showPlaylistDialog(context, spotify),
|
|
);
|
|
}
|
|
|
|
return Button.secondary(
|
|
leading: const Icon(SpotubeIcons.addFilled),
|
|
child: Text(context.l10n.create_playlist),
|
|
onPressed: () => showPlaylistDialog(context, spotify),
|
|
);
|
|
}
|
|
}
|