mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
refactor: use shadcn widgets for create playlist and add tracks to playlist dialog
This commit is contained in:
parent
047eccfa82
commit
684e595d16
@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
||||||
@ -22,7 +21,7 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final ThemeData(:textTheme) = Theme.of(context);
|
final typography = Theme.of(context).typography;
|
||||||
final userPlaylists = ref.watch(favoritePlaylistsProvider);
|
final userPlaylists = ref.watch(favoritePlaylistsProvider);
|
||||||
final favoritePlaylistsNotifier =
|
final favoritePlaylistsNotifier =
|
||||||
ref.watch(favoritePlaylistsProvider.notifier);
|
ref.watch(favoritePlaylistsProvider.notifier);
|
||||||
@ -64,37 +63,38 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
|
|||||||
tracks.map((e) => e.id!).toList(),
|
tracks.map((e) => e.id!).toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).then((_) => Navigator.pop(context, true));
|
).then((_) => context.mounted ? Navigator.pop(context, true) : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return AlertDialog(
|
return ConstrainedBox(
|
||||||
insetPadding: EdgeInsets.zero,
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
|
child: AlertDialog(
|
||||||
title: Row(
|
title: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
context.l10n.add_to_playlist,
|
context.l10n.add_to_playlist,
|
||||||
style: textTheme.titleMedium,
|
style: typography.large,
|
||||||
),
|
),
|
||||||
const Gap(20),
|
const Spacer(),
|
||||||
const PlaylistCreateDialogButton(),
|
const PlaylistCreateDialogButton(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
OutlinedButton(
|
OutlineButton(
|
||||||
child: Text(context.l10n.cancel),
|
child: Text(context.l10n.cancel),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context, false);
|
Navigator.pop(context, false);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
FilledButton(
|
PrimaryButton(
|
||||||
onPressed: onAdd,
|
onPressed: onAdd,
|
||||||
child: Text(context.l10n.add),
|
child: Text(context.l10n.add),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
height: 300,
|
height: 300,
|
||||||
width: 300,
|
|
||||||
child: userPlaylists.isLoading
|
child: userPlaylists.isLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: ListView.builder(
|
: ListView.builder(
|
||||||
@ -102,30 +102,48 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
|
|||||||
itemCount: filteredPlaylists.length,
|
itemCount: filteredPlaylists.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final playlist = filteredPlaylists.elementAt(index);
|
final playlist = filteredPlaylists.elementAt(index);
|
||||||
return CheckboxListTile(
|
return Button.ghost(
|
||||||
secondary: CircleAvatar(
|
style: ButtonVariance.ghost.copyWith(
|
||||||
backgroundImage: UniversalImage.imageProvider(
|
padding: (context, _, __) {
|
||||||
|
return const EdgeInsets.symmetric(vertical: 8);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
leading: Avatar(
|
||||||
|
initials:
|
||||||
|
Avatar.getInitials(playlist.name ?? "Playlist"),
|
||||||
|
provider: UniversalImage.imageProvider(
|
||||||
playlist.images.asUrlString(
|
playlist.images.asUrlString(
|
||||||
placeholder: ImagePlaceholder.collection,
|
placeholder: ImagePlaceholder.collection,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
contentPadding: EdgeInsets.zero,
|
trailing: Checkbox(
|
||||||
title: Padding(
|
state: (playlistsCheck.value[playlist.id] ?? false)
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
? CheckboxState.checked
|
||||||
child: Text(playlist.name!),
|
: CheckboxState.unchecked,
|
||||||
),
|
|
||||||
value: playlistsCheck.value[playlist.id] ?? false,
|
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
playlistsCheck.value = {
|
playlistsCheck.value = {
|
||||||
...playlistsCheck.value,
|
...playlistsCheck.value,
|
||||||
playlist.id!: val == true
|
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!),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
45
lib/components/form/checkbox_form_field.dart
Normal file
45
lib/components/form/checkbox_form_field.dart
Normal file
@ -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<bool>? validator;
|
||||||
|
|
||||||
|
final ValueChanged<CheckboxState>? 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<bool>(
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
187
lib/components/form/text_form_field.dart
Normal file
187
lib/components/form/text_form_field.dart
Normal file
@ -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<String>? 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<String>? 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<String>? onChanged;
|
||||||
|
final Iterable<String>? autofillHints;
|
||||||
|
final void Function(PointerDownEvent event)? onTapOutside;
|
||||||
|
final List<TextInputFormatter>? 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<String>(
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -90,7 +90,7 @@ class TrackPresentation extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const PresentationListSection(),
|
const PresentationListSection(),
|
||||||
const SliverGap(200),
|
const SliverSafeArea(sliver: SliverGap(10)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -2,20 +2,23 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
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:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:form_validator/form_validator.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:image_picker/image_picker.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:spotify/spotify.dart';
|
||||||
|
|
||||||
import 'package:spotube/collections/spotube_icons.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/components/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/image.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/spotify.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
|
|
||||||
@ -23,25 +26,23 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
/// Track ids to add to the playlist
|
/// Track ids to add to the playlist
|
||||||
final List<String> trackIds;
|
final List<String> trackIds;
|
||||||
final String? playlistId;
|
final String? playlistId;
|
||||||
PlaylistCreateDialog({
|
const PlaylistCreateDialog({
|
||||||
super.key,
|
super.key,
|
||||||
this.trackIds = const [],
|
this.trackIds = const [],
|
||||||
this.playlistId,
|
this.playlistId,
|
||||||
});
|
});
|
||||||
|
|
||||||
final formKey = GlobalKey<FormState>();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
return ScaffoldMessenger(
|
|
||||||
child: Scaffold(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
body: HookBuilder(builder: (context) {
|
|
||||||
final userPlaylists = ref.watch(favoritePlaylistsProvider);
|
final userPlaylists = ref.watch(favoritePlaylistsProvider);
|
||||||
final playlist = ref.watch(playlistProvider(playlistId ?? ""));
|
final playlist = ref.watch(playlistProvider(playlistId ?? ""));
|
||||||
final playlistNotifier =
|
final playlistNotifier =
|
||||||
ref.watch(playlistProvider(playlistId ?? "").notifier);
|
ref.watch(playlistProvider(playlistId ?? "").notifier);
|
||||||
|
|
||||||
|
final isSubmitting = useState(false);
|
||||||
|
|
||||||
|
final formKey = useMemoized(() => GlobalKey<FormBuilderState>(), []);
|
||||||
|
|
||||||
final updatingPlaylist = useMemoized(
|
final updatingPlaylist = useMemoized(
|
||||||
() => userPlaylists.asData?.value.items
|
() => userPlaylists.asData?.value.items
|
||||||
.firstWhereOrNull((playlist) => playlist.id == playlistId),
|
.firstWhereOrNull((playlist) => playlist.id == playlistId),
|
||||||
@ -51,52 +52,46 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
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<XFile?>(null);
|
|
||||||
|
|
||||||
final isUpdatingPlaylist = playlistId != null;
|
final isUpdatingPlaylist = playlistId != null;
|
||||||
|
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final scaffold = ScaffoldMessenger.of(context);
|
|
||||||
|
|
||||||
final onError = useCallback((error) {
|
final onError = useCallback((error) {
|
||||||
if (error is SpotifyError || error is SpotifyException) {
|
if (error is SpotifyError || error is SpotifyException) {
|
||||||
scaffold.showSnackBar(
|
showToast(
|
||||||
SnackBar(
|
context: context,
|
||||||
content: Text(
|
location: ToastLocation.topRight,
|
||||||
l10n.error(error.message ?? context.l10n.epic_failure),
|
builder: (context, overlay) {
|
||||||
style: theme.textTheme.bodyMedium!.copyWith(
|
return SurfaceCard(
|
||||||
color: theme.colorScheme.onError,
|
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]);
|
}, [l10n, theme]);
|
||||||
|
|
||||||
Future<void> onCreate() async {
|
Future<void> onCreate() async {
|
||||||
if (!formKey.currentState!.validate()) return;
|
if (!formKey.currentState!.saveAndValidate()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isSubmitting.value = true;
|
||||||
|
final values = formKey.currentState!.value;
|
||||||
|
|
||||||
final PlaylistInput payload = (
|
final PlaylistInput payload = (
|
||||||
playlistName: playlistName.text,
|
playlistName: values['playlistName'],
|
||||||
collaborative: collaborative.value,
|
collaborative: values['collaborative'],
|
||||||
public: public.value,
|
public: values['public'],
|
||||||
description: description.text,
|
description: values['description'],
|
||||||
base64Image: image.value?.path != null
|
base64Image: (values['image'] as XFile?)?.path != null
|
||||||
? await image.value!
|
? await (values['image'] as XFile)
|
||||||
.readAsBytes()
|
.readAsBytes()
|
||||||
.then((bytes) => base64Encode(bytes))
|
.then((bytes) => base64Encode(bytes))
|
||||||
: null,
|
: null,
|
||||||
@ -107,12 +102,14 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
} else {
|
} else {
|
||||||
await playlistNotifier.create(payload, onError);
|
await playlistNotifier.create(payload, onError);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
if (context.mounted &&
|
if (context.mounted &&
|
||||||
!ref.read(playlistProvider(playlistId ?? "")).hasError) {
|
!ref.read(playlistProvider(playlistId ?? "")).hasError) {
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(
|
title: Text(
|
||||||
@ -121,35 +118,36 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
: context.l10n.create_a_playlist,
|
: context.l10n.create_a_playlist,
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
OutlinedButton(
|
Button.outline(
|
||||||
child: Text(context.l10n.cancel),
|
child: Text(context.l10n.cancel),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
FilledButton(
|
Button.primary(
|
||||||
onPressed: playlist.isLoading ? null : onCreate,
|
onPressed: onCreate,
|
||||||
|
enabled: !playlist.isLoading & !isSubmitting.value,
|
||||||
child: Text(
|
child: Text(
|
||||||
isUpdatingPlaylist
|
isUpdatingPlaylist ? context.l10n.update : context.l10n.create,
|
||||||
? context.l10n.update
|
|
||||||
: context.l10n.create,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
insetPadding: const EdgeInsets.all(8),
|
|
||||||
content: Container(
|
content: Container(
|
||||||
width: MediaQuery.of(context).size.width,
|
width: MediaQuery.of(context).size.width,
|
||||||
constraints: const BoxConstraints(maxWidth: 500),
|
constraints: const BoxConstraints(maxWidth: 500),
|
||||||
child: Form(
|
child: FormBuilder(
|
||||||
key: formKey,
|
key: formKey,
|
||||||
|
initialValue: {
|
||||||
|
'playlistName': updatingPlaylist?.name,
|
||||||
|
'description': updatingPlaylist?.description,
|
||||||
|
'public': updatingPlaylist?.public ?? false,
|
||||||
|
'collaborative': updatingPlaylist?.collaborative ?? false,
|
||||||
|
},
|
||||||
child: ListView(
|
child: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
children: [
|
children: [
|
||||||
FormField<XFile?>(
|
FormBuilderField<XFile?>(
|
||||||
initialValue: image.value,
|
name: 'image',
|
||||||
onSaved: (newValue) {
|
|
||||||
image.value = newValue;
|
|
||||||
},
|
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
final file = File(value.path);
|
final file = File(value.path);
|
||||||
@ -157,10 +155,15 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
if (file.lengthSync() > 256000) {
|
if (file.lengthSync() > 256000) {
|
||||||
return "Image size should be less than 256kb";
|
return "Image size should be less than 256kb";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (extension(file.path) != ".png") {
|
||||||
|
return "Image should be in PNG format";
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
builder: (field) {
|
builder: (field) {
|
||||||
return Column(
|
return Column(
|
||||||
|
spacing: 10,
|
||||||
children: [
|
children: [
|
||||||
UniversalImage(
|
UniversalImage(
|
||||||
path: field.value?.path ??
|
path: field.value?.path ??
|
||||||
@ -169,22 +172,21 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
height: 200,
|
height: 200,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
FilledButton.icon(
|
Button.secondary(
|
||||||
icon: const Icon(SpotubeIcons.edit),
|
leading: const Icon(SpotubeIcons.edit),
|
||||||
label: Text(
|
child: Text(
|
||||||
field.value?.path != null ||
|
field.value?.path != null ||
|
||||||
updatingPlaylist?.images != null
|
updatingPlaylist?.images != null
|
||||||
? context.l10n.change_cover
|
? context.l10n.change_cover
|
||||||
: context.l10n.add_cover,
|
: context.l10n.add_cover,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final imageFile = await ImagePicker()
|
final imageFile = await ImagePicker().pickImage(
|
||||||
.pickImage(
|
source: ImageSource.gallery,
|
||||||
source: ImageSource.gallery);
|
);
|
||||||
|
|
||||||
if (imageFile != null) {
|
if (imageFile != null) {
|
||||||
field.didChange(imageFile);
|
field.didChange(imageFile);
|
||||||
@ -194,16 +196,10 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
IconButton.filled(
|
IconButton.destructive(
|
||||||
icon: const Icon(SpotubeIcons.trash),
|
icon: const Icon(SpotubeIcons.trash),
|
||||||
style: IconButton.styleFrom(
|
enabled: field.value != null,
|
||||||
backgroundColor:
|
onPressed: () {
|
||||||
theme.colorScheme.errorContainer,
|
|
||||||
foregroundColor: theme.colorScheme.error,
|
|
||||||
),
|
|
||||||
onPressed: field.value == null
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
field.didChange(null);
|
field.didChange(null);
|
||||||
field.validate();
|
field.validate();
|
||||||
field.save();
|
field.save();
|
||||||
@ -214,52 +210,45 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
if (field.hasError)
|
if (field.hasError)
|
||||||
Text(
|
Text(
|
||||||
field.errorText ?? "",
|
field.errorText ?? "",
|
||||||
style: theme.textTheme.bodyMedium!.copyWith(
|
style: theme.typography.normal.copyWith(
|
||||||
color: theme.colorScheme.error,
|
color: theme.colorScheme.destructive,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
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 Gap(20),
|
||||||
),
|
TextFormBuilderField(
|
||||||
const SizedBox(height: 10),
|
name: 'playlistName',
|
||||||
TextFormField(
|
label: Text(context.l10n.playlist_name),
|
||||||
controller: description,
|
placeholder: Text(context.l10n.name_of_playlist),
|
||||||
decoration: InputDecoration(
|
validator: FormBuilderValidators.required(),
|
||||||
hintText: context.l10n.description,
|
|
||||||
),
|
),
|
||||||
|
const Gap(20),
|
||||||
|
TextFormBuilderField(
|
||||||
|
name: 'description',
|
||||||
|
label: Text(context.l10n.description),
|
||||||
|
validator: FormBuilderValidators.required(),
|
||||||
|
placeholder: Text(context.l10n.description),
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
validator: ValidationBuilder().required().build(),
|
|
||||||
maxLines: 5,
|
maxLines: 5,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const Gap(20),
|
||||||
CheckboxListTile(
|
CheckboxFormBuilderField(
|
||||||
title: Text(context.l10n.public),
|
name: 'public',
|
||||||
value: public.value,
|
trailing: Text(context.l10n.public),
|
||||||
onChanged: (val) => public.value = val ?? false,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const Gap(10),
|
||||||
CheckboxListTile(
|
CheckboxFormBuilderField(
|
||||||
title: Text(context.l10n.collaborative),
|
name: 'collaborative',
|
||||||
value: collaborative.value,
|
trailing: Text(context.l10n.collaborative),
|
||||||
onChanged: (val) => collaborative.value = val ?? false,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,7 +258,10 @@ class PlaylistCreateDialogButton extends HookConsumerWidget {
|
|||||||
showPlaylistDialog(BuildContext context, SpotifyApi spotify) {
|
showPlaylistDialog(BuildContext context, SpotifyApi spotify) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
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);
|
final spotify = ref.watch(spotifyProvider);
|
||||||
|
|
||||||
if (mediaQuery.smAndDown) {
|
if (mediaQuery.smAndDown) {
|
||||||
return ElevatedButton(
|
return IconButton.secondary(
|
||||||
style: FilledButton.styleFrom(
|
icon: const Icon(SpotubeIcons.addFilled),
|
||||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
child: const Icon(SpotubeIcons.addFilled),
|
|
||||||
onPressed: () => showPlaylistDialog(context, spotify),
|
onPressed: () => showPlaylistDialog(context, spotify),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return FilledButton.tonalIcon(
|
return Button.secondary(
|
||||||
style: FilledButton.styleFrom(
|
leading: const Icon(SpotubeIcons.addFilled),
|
||||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
child: Text(context.l10n.create_playlist),
|
||||||
),
|
|
||||||
icon: const Icon(SpotubeIcons.addFilled),
|
|
||||||
label: Text(context.l10n.create_playlist),
|
|
||||||
onPressed: () => showPlaylistDialog(context, spotify),
|
onPressed: () => showPlaylistDialog(context, spotify),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
18
pubspec.lock
18
pubspec.lock
@ -757,6 +757,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0+1"
|
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:
|
flutter_gen_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -972,6 +980,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
form_validator:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -2670,5 +2686,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.6"
|
version: "2.3.6"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.5.3 <4.0.0"
|
dart: ">=3.6.0 <4.0.0"
|
||||||
flutter: ">=3.27.0"
|
flutter: ">=3.27.0"
|
||||||
|
@ -54,6 +54,7 @@ dependencies:
|
|||||||
flutter_discord_rpc: ^1.0.0
|
flutter_discord_rpc: ^1.0.0
|
||||||
flutter_displaymode: ^0.6.0
|
flutter_displaymode: ^0.6.0
|
||||||
flutter_feather_icons: ^2.0.0+1
|
flutter_feather_icons: ^2.0.0+1
|
||||||
|
flutter_form_builder: ^9.6.0
|
||||||
flutter_hooks: ^0.20.5
|
flutter_hooks: ^0.20.5
|
||||||
flutter_inappwebview: ^6.1.3
|
flutter_inappwebview: ^6.1.3
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
@ -62,6 +63,7 @@ dependencies:
|
|||||||
flutter_riverpod: ^2.5.1
|
flutter_riverpod: ^2.5.1
|
||||||
flutter_secure_storage: ^9.0.0
|
flutter_secure_storage: ^9.0.0
|
||||||
flutter_sharing_intent: ^1.1.0
|
flutter_sharing_intent: ^1.1.0
|
||||||
|
form_builder_validators: ^11.1.1
|
||||||
form_validator: ^2.1.1
|
form_validator: ^2.1.1
|
||||||
freezed_annotation: ^2.4.1
|
freezed_annotation: ^2.4.1
|
||||||
fuzzywuzzy: ^1.1.6
|
fuzzywuzzy: ^1.1.6
|
||||||
|
Loading…
Reference in New Issue
Block a user