From 684e595d1687ec422695d6cd29737ab726ab8f03 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 29 Dec 2024 15:07:48 +0600 Subject: [PATCH] refactor: use shadcn widgets for create playlist and add tracks to playlist dialog --- .../dialogs/playlist_add_track_dialog.dart | 134 +++--- lib/components/form/checkbox_form_field.dart | 45 ++ lib/components/form/text_form_field.dart | 187 ++++++++ .../track_presentation.dart | 2 +- .../playlist/playlist_create_dialog.dart | 442 +++++++++--------- pubspec.lock | 18 +- pubspec.yaml | 2 + 7 files changed, 542 insertions(+), 288 deletions(-) create mode 100644 lib/components/form/checkbox_form_field.dart create mode 100644 lib/components/form/text_form_field.dart diff --git a/lib/components/dialogs/playlist_add_track_dialog.dart b/lib/components/dialogs/playlist_add_track_dialog.dart index 5af9c9e4..5098bf9d 100644 --- a/lib/components/dialogs/playlist_add_track_dialog.dart +++ b/lib/components/dialogs/playlist_add_track_dialog.dart @@ -1,7 +1,6 @@ -import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/modules/playlist/playlist_create_dialog.dart'; @@ -22,7 +21,7 @@ class PlaylistAddTrackDialog extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final ThemeData(:textTheme) = Theme.of(context); + final typography = Theme.of(context).typography; final userPlaylists = ref.watch(favoritePlaylistsProvider); final favoritePlaylistsNotifier = ref.watch(favoritePlaylistsProvider.notifier); @@ -64,67 +63,86 @@ class PlaylistAddTrackDialog extends HookConsumerWidget { tracks.map((e) => e.id!).toList(), ), ), - ).then((_) => Navigator.pop(context, true)); + ).then((_) => context.mounted ? Navigator.pop(context, true) : null); } - return AlertDialog( - insetPadding: EdgeInsets.zero, - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - context.l10n.add_to_playlist, - style: textTheme.titleMedium, + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: AlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.l10n.add_to_playlist, + style: typography.large, + ), + const Spacer(), + const PlaylistCreateDialogButton(), + ], + ), + actions: [ + OutlineButton( + child: Text(context.l10n.cancel), + onPressed: () { + Navigator.pop(context, false); + }, + ), + PrimaryButton( + onPressed: onAdd, + child: Text(context.l10n.add), ), - const Gap(20), - const PlaylistCreateDialogButton(), ], - ), - actions: [ - OutlinedButton( - child: Text(context.l10n.cancel), - onPressed: () { - Navigator.pop(context, false); - }, - ), - FilledButton( - onPressed: onAdd, - child: Text(context.l10n.add), - ), - ], - content: SizedBox( - height: 300, - width: 300, - child: userPlaylists.isLoading - ? const Center(child: CircularProgressIndicator()) - : ListView.builder( - shrinkWrap: true, - itemCount: filteredPlaylists.length, - itemBuilder: (context, index) { - final playlist = filteredPlaylists.elementAt(index); - return CheckboxListTile( - secondary: CircleAvatar( - backgroundImage: UniversalImage.imageProvider( - playlist.images.asUrlString( - placeholder: ImagePlaceholder.collection, + content: SizedBox( + height: 300, + child: userPlaylists.isLoading + ? const Center(child: CircularProgressIndicator()) + : ListView.builder( + shrinkWrap: true, + itemCount: filteredPlaylists.length, + itemBuilder: (context, index) { + final playlist = filteredPlaylists.elementAt(index); + return Button.ghost( + style: ButtonVariance.ghost.copyWith( + padding: (context, _, __) { + return const EdgeInsets.symmetric(vertical: 8); + }, + ), + leading: Avatar( + initials: + Avatar.getInitials(playlist.name ?? "Playlist"), + provider: UniversalImage.imageProvider( + playlist.images.asUrlString( + placeholder: ImagePlaceholder.collection, + ), ), ), - ), - contentPadding: EdgeInsets.zero, - title: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text(playlist.name!), - ), - value: playlistsCheck.value[playlist.id] ?? false, - onChanged: (val) { - playlistsCheck.value = { - ...playlistsCheck.value, - playlist.id!: val == true - }; - }, - ); - }, - ), + trailing: Checkbox( + state: (playlistsCheck.value[playlist.id] ?? false) + ? CheckboxState.checked + : CheckboxState.unchecked, + onChanged: (val) { + playlistsCheck.value = { + ...playlistsCheck.value, + playlist.id!: val == CheckboxState.checked, + }; + }, + ), + onPressed: () { + playlistsCheck.value = { + ...playlistsCheck.value, + playlist.id!: + !(playlistsCheck.value[playlist.id] ?? false), + }; + }, + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text(playlist.name!), + ), + ); + }, + ), + ), ), ); } diff --git a/lib/components/form/checkbox_form_field.dart b/lib/components/form/checkbox_form_field.dart new file mode 100644 index 00000000..0e794833 --- /dev/null +++ b/lib/components/form/checkbox_form_field.dart @@ -0,0 +1,45 @@ +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +class CheckboxFormBuilderField extends StatelessWidget { + final String name; + final FormFieldValidator? validator; + + final ValueChanged? onChanged; + final Widget? leading; + final Widget? trailing; + final bool tristate; + const CheckboxFormBuilderField({ + super.key, + required this.name, + this.validator, + this.onChanged, + this.leading, + this.trailing, + this.tristate = false, + }); + + @override + Widget build(BuildContext context) { + return FormBuilderField( + name: name, + validator: validator, + builder: (field) { + return Checkbox( + state: tristate && field.value == null + ? CheckboxState.indeterminate + : field.value == true + ? CheckboxState.checked + : CheckboxState.unchecked, + onChanged: (state) { + field.didChange(state == CheckboxState.checked); + onChanged?.call(state); + }, + leading: leading, + trailing: trailing, + tristate: tristate, + ); + }, + ); + } +} diff --git a/lib/components/form/text_form_field.dart b/lib/components/form/text_form_field.dart new file mode 100644 index 00000000..ef3514c5 --- /dev/null +++ b/lib/components/form/text_form_field.dart @@ -0,0 +1,187 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; + +class TextFormBuilderField extends StatelessWidget { + final String name; + final FormFieldValidator? validator; + final Widget? label; + + final TextEditingController? controller; + final bool filled; + final Widget? placeholder; + final AlignmentGeometry? placeholderAlignment; + final AlignmentGeometry? leadingAlignment; + final AlignmentGeometry? trailingAlignment; + final bool border; + final Widget? leading; + final Widget? trailing; + final EdgeInsetsGeometry? padding; + final ValueChanged? onSubmitted; + final VoidCallback? onEditingComplete; + final FocusNode? focusNode; + final VoidCallback? onTap; + final bool enabled; + final bool readOnly; + final bool obscureText; + final String obscuringCharacter; + final String? initialValue; + final int? maxLength; + final MaxLengthEnforcement? maxLengthEnforcement; + final int? maxLines; + final int? minLines; + final BorderRadiusGeometry? borderRadius; + final TextAlign textAlign; + final bool expands; + final TextAlignVertical? textAlignVertical; + final UndoHistoryController? undoController; + final ValueChanged? onChanged; + final Iterable? autofillHints; + final void Function(PointerDownEvent event)? onTapOutside; + final List? inputFormatters; + final TextStyle? style; + final EditableTextContextMenuBuilder? contextMenuBuilder; + final bool useNativeContextMenu; + final bool? isCollapsed; + final TextInputType? keyboardType; + final TextInputAction? textInputAction; + final Clip clipBehavior; + final bool autofocus; + final WidgetStatesController? statesController; + + const TextFormBuilderField({ + super.key, + required this.name, + this.label, + this.validator, + this.controller, + this.maxLength, + this.maxLengthEnforcement, + this.maxLines = 1, + this.minLines, + this.filled = false, + this.placeholder, + this.border = true, + this.leading, + this.trailing, + this.padding, + this.onSubmitted, + this.onEditingComplete, + this.focusNode, + this.onTap, + this.enabled = true, + this.readOnly = false, + this.obscureText = false, + this.obscuringCharacter = '•', + this.initialValue, + this.borderRadius, + this.keyboardType, + this.textAlign = TextAlign.start, + this.expands = false, + this.textAlignVertical = TextAlignVertical.center, + this.autofillHints, + this.undoController, + this.onChanged, + this.onTapOutside, + this.inputFormatters, + this.style, + this.contextMenuBuilder = TextField.defaultContextMenuBuilder, + this.useNativeContextMenu = false, + this.isCollapsed, + this.textInputAction, + this.clipBehavior = Clip.hardEdge, + this.autofocus = false, + this.placeholderAlignment, + this.leadingAlignment, + this.trailingAlignment, + this.statesController, + }); + + @override + Widget build(BuildContext context) { + return FormBuilderField( + name: name, + validator: validator, + onChanged: (value) { + if (value == null) return; + onChanged?.call(value); + }, + builder: (field) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: 5, + children: [ + if (label != null) + DefaultTextStyle( + style: context.theme.typography.semiBold.copyWith( + color: field.hasError + ? context.theme.colorScheme.destructive + : context.theme.colorScheme.foreground, + ), + child: label!, + ), + TextField( + controller: controller, + maxLength: maxLength, + maxLengthEnforcement: maxLengthEnforcement, + maxLines: maxLines, + minLines: minLines, + filled: filled, + placeholder: placeholder, + border: border, + leading: leading, + trailing: trailing, + padding: padding, + onSubmitted: (value) { + field.validate(); + field.save(); + onSubmitted?.call(value); + }, + onEditingComplete: () { + field.save(); + onEditingComplete?.call(); + }, + focusNode: focusNode, + onTap: onTap, + enabled: enabled, + readOnly: readOnly, + obscureText: obscureText, + obscuringCharacter: obscuringCharacter, + initialValue: field.value, + borderRadius: borderRadius, + textAlign: textAlign, + expands: expands, + textAlignVertical: textAlignVertical, + autofillHints: autofillHints, + undoController: undoController, + onChanged: (value) { + field.didChange(value); + }, + onTapOutside: onTapOutside, + inputFormatters: inputFormatters, + style: style, + contextMenuBuilder: contextMenuBuilder, + useNativeContextMenu: useNativeContextMenu, + isCollapsed: isCollapsed, + keyboardType: keyboardType, + textInputAction: textInputAction, + clipBehavior: clipBehavior, + autofocus: autofocus, + placeholderAlignment: placeholderAlignment, + leadingAlignment: leadingAlignment, + trailingAlignment: trailingAlignment, + statesController: statesController, + ), + if (field.hasError) + Text( + field.errorText ?? "", + style: TextStyle( + color: context.theme.colorScheme.destructive, + ), + ), + ], + ), + ); + } +} diff --git a/lib/components/track_presentation/track_presentation.dart b/lib/components/track_presentation/track_presentation.dart index e81a2e1e..96f5f964 100644 --- a/lib/components/track_presentation/track_presentation.dart +++ b/lib/components/track_presentation/track_presentation.dart @@ -90,7 +90,7 @@ class TrackPresentation extends HookConsumerWidget { }, ), const PresentationListSection(), - const SliverGap(200), + const SliverSafeArea(sliver: SliverGap(10)), ], ), ), diff --git a/lib/modules/playlist/playlist_create_dialog.dart b/lib/modules/playlist/playlist_create_dialog.dart index 78680a1c..8b231b84 100644 --- a/lib/modules/playlist/playlist_create_dialog.dart +++ b/lib/modules/playlist/playlist_create_dialog.dart @@ -2,20 +2,23 @@ import 'dart:convert'; import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:flutter/material.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:form_validator/form_validator.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/extensions/string.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify_provider.dart'; @@ -23,241 +26,227 @@ class PlaylistCreateDialog extends HookConsumerWidget { /// Track ids to add to the playlist final List trackIds; final String? playlistId; - PlaylistCreateDialog({ + const PlaylistCreateDialog({ super.key, this.trackIds = const [], this.playlistId, }); - final formKey = GlobalKey(); - @override Widget build(BuildContext context, ref) { - return ScaffoldMessenger( - child: Scaffold( - backgroundColor: Colors.transparent, - body: HookBuilder(builder: (context) { - final userPlaylists = ref.watch(favoritePlaylistsProvider); - final playlist = ref.watch(playlistProvider(playlistId ?? "")); - final playlistNotifier = - ref.watch(playlistProvider(playlistId ?? "").notifier); + final userPlaylists = ref.watch(favoritePlaylistsProvider); + final playlist = ref.watch(playlistProvider(playlistId ?? "")); + final playlistNotifier = + ref.watch(playlistProvider(playlistId ?? "").notifier); - final updatingPlaylist = useMemoized( - () => userPlaylists.asData?.value.items - .firstWhereOrNull((playlist) => playlist.id == playlistId), - [ - userPlaylists.asData?.value.items, - playlistId, - ], - ); + final isSubmitting = useState(false); - final playlistName = useTextEditingController( - text: updatingPlaylist?.name, - ); - final description = useTextEditingController( - text: updatingPlaylist?.description?.unescapeHtml(), - ); - final public = useState( - updatingPlaylist?.public ?? false, - ); - final collaborative = useState( - updatingPlaylist?.collaborative ?? false, - ); - final image = useState(null); + final formKey = useMemoized(() => GlobalKey(), []); - final isUpdatingPlaylist = playlistId != null; + final updatingPlaylist = useMemoized( + () => userPlaylists.asData?.value.items + .firstWhereOrNull((playlist) => playlist.id == playlistId), + [ + userPlaylists.asData?.value.items, + playlistId, + ], + ); - final l10n = context.l10n; - final theme = Theme.of(context); - final scaffold = ScaffoldMessenger.of(context); + final isUpdatingPlaylist = playlistId != null; - final onError = useCallback((error) { - if (error is SpotifyError || error is SpotifyException) { - scaffold.showSnackBar( - SnackBar( - content: Text( - l10n.error(error.message ?? context.l10n.epic_failure), - style: theme.textTheme.bodyMedium!.copyWith( - color: theme.colorScheme.onError, - ), + 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, ), - backgroundColor: theme.colorScheme.error, ), - ); - } - }, [scaffold, l10n, theme]); - - Future onCreate() async { - if (!formKey.currentState!.validate()) return; - - final PlaylistInput payload = ( - playlistName: playlistName.text, - collaborative: collaborative.value, - public: public.value, - description: description.text, - base64Image: image.value?.path != null - ? await image.value! - .readAsBytes() - .then((bytes) => base64Encode(bytes)) - : null, + ), ); + }, + ); + } + }, [l10n, theme]); - if (isUpdatingPlaylist) { - await playlistNotifier.modify(payload, onError); - } else { - await playlistNotifier.create(payload, onError); - } + Future onCreate() async { + if (!formKey.currentState!.saveAndValidate()) return; - if (context.mounted && - !ref.read(playlistProvider(playlistId ?? "")).hasError) { - context.pop(); - } - } + try { + isSubmitting.value = true; + final values = formKey.currentState!.value; - return AlertDialog( - title: Text( - isUpdatingPlaylist - ? context.l10n.update_playlist - : context.l10n.create_a_playlist, - ), - actions: [ - OutlinedButton( - child: Text(context.l10n.cancel), - onPressed: () { - Navigator.pop(context); + 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( + 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, + ), + ) + ], + ); }, ), - FilledButton( - onPressed: playlist.isLoading ? null : onCreate, - child: Text( - isUpdatingPlaylist - ? context.l10n.update - : context.l10n.create, - ), + 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), ), ], - insetPadding: const EdgeInsets.all(8), - content: Container( - width: MediaQuery.of(context).size.width, - constraints: const BoxConstraints(maxWidth: 500), - child: Form( - key: formKey, - child: ListView( - shrinkWrap: true, - children: [ - FormField( - initialValue: image.value, - onSaved: (newValue) { - image.value = newValue; - }, - validator: (value) { - if (value == null) return null; - final file = File(value.path); - - if (file.lengthSync() > 256000) { - return "Image size should be less than 256kb"; - } - return null; - }, - builder: (field) { - return Column( - children: [ - UniversalImage( - path: field.value?.path ?? - (updatingPlaylist?.images).asUrlString( - placeholder: ImagePlaceholder.collection, - ), - height: 200, - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FilledButton.icon( - icon: const Icon(SpotubeIcons.edit), - label: 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.filled( - icon: const Icon(SpotubeIcons.trash), - style: IconButton.styleFrom( - backgroundColor: - theme.colorScheme.errorContainer, - foregroundColor: theme.colorScheme.error, - ), - onPressed: field.value == null - ? null - : () { - field.didChange(null); - field.validate(); - field.save(); - }, - ), - ], - ), - if (field.hasError) - Text( - field.errorText ?? "", - style: theme.textTheme.bodyMedium!.copyWith( - color: theme.colorScheme.error, - ), - ) - ], - ); - }), - const SizedBox(height: 10), - TextFormField( - controller: playlistName, - decoration: InputDecoration( - hintText: context.l10n.name_of_playlist, - labelText: context.l10n.name_of_playlist, - ), - validator: ValidationBuilder().required().build(), - ), - const SizedBox(height: 10), - TextFormField( - controller: description, - decoration: InputDecoration( - hintText: context.l10n.description, - ), - keyboardType: TextInputType.multiline, - validator: ValidationBuilder().required().build(), - 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, - ), - ], - ), - ), - ), - ); - }), + ), + ), ), ); } @@ -269,7 +258,10 @@ class PlaylistCreateDialogButton extends HookConsumerWidget { showPlaylistDialog(BuildContext context, SpotifyApi spotify) { showDialog( context: context, - builder: (context) => PlaylistCreateDialog(), + alignment: Alignment.center, + builder: (context) => const ToastLayer( + child: PlaylistCreateDialog(), + ), ); } @@ -279,21 +271,15 @@ class PlaylistCreateDialogButton extends HookConsumerWidget { final spotify = ref.watch(spotifyProvider); if (mediaQuery.smAndDown) { - return ElevatedButton( - style: FilledButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.primary, - ), - child: const Icon(SpotubeIcons.addFilled), + return IconButton.secondary( + icon: const Icon(SpotubeIcons.addFilled), onPressed: () => showPlaylistDialog(context, spotify), ); } - return FilledButton.tonalIcon( - style: FilledButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.primary, - ), - icon: const Icon(SpotubeIcons.addFilled), - label: Text(context.l10n.create_playlist), + return Button.secondary( + leading: const Icon(SpotubeIcons.addFilled), + child: Text(context.l10n.create_playlist), onPressed: () => showPlaylistDialog(context, spotify), ); } diff --git a/pubspec.lock b/pubspec.lock index d441371d..8c8c30be 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -757,6 +757,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0+1" + flutter_form_builder: + dependency: "direct main" + description: + name: flutter_form_builder + sha256: "39aee5a2548df0b3979a83eea38468116a888341fbca8a92c4be18a486a7bb57" + url: "https://pub.dev" + source: hosted + version: "9.6.0" flutter_gen_core: dependency: transitive description: @@ -972,6 +980,14 @@ packages: description: flutter source: sdk version: "0.0.0" + form_builder_validators: + dependency: "direct main" + description: + name: form_builder_validators + sha256: "517fb884183fff7a0ef3db7d375981011da26ee452f20fb3d2e788ad527ad01d" + url: "https://pub.dev" + source: hosted + version: "11.1.1" form_validator: dependency: "direct main" description: @@ -2670,5 +2686,5 @@ packages: source: hosted version: "2.3.6" sdks: - dart: ">=3.5.3 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index f00c298f..e06cf96c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,7 @@ dependencies: flutter_discord_rpc: ^1.0.0 flutter_displaymode: ^0.6.0 flutter_feather_icons: ^2.0.0+1 + flutter_form_builder: ^9.6.0 flutter_hooks: ^0.20.5 flutter_inappwebview: ^6.1.3 flutter_localizations: @@ -62,6 +63,7 @@ dependencies: flutter_riverpod: ^2.5.1 flutter_secure_storage: ^9.0.0 flutter_sharing_intent: ^1.1.0 + form_builder_validators: ^11.1.1 form_validator: ^2.1.1 freezed_annotation: ^2.4.1 fuzzywuzzy: ^1.1.6