mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45: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: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!),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
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 SliverGap(200),
|
||||
const SliverSafeArea(sliver: SliverGap(10)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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<String> trackIds;
|
||||
final String? playlistId;
|
||||
PlaylistCreateDialog({
|
||||
const PlaylistCreateDialog({
|
||||
super.key,
|
||||
this.trackIds = const [],
|
||||
this.playlistId,
|
||||
});
|
||||
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
@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<XFile?>(null);
|
||||
final formKey = useMemoized(() => GlobalKey<FormBuilderState>(), []);
|
||||
|
||||
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<void> 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<void> 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<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,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
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<XFile?>(
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
18
pubspec.lock
18
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"
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user