spotube/lib/modules/playlist/playlist_create_dialog.dart
Alessio fece073def This pull request primarily involves the removal of several configuration files and assets, as well as minor updates to documentation. The most significant changes are the deletion of various .vscode configuration files and the removal of unused assets from the project.
Configuration File Removals:

    .vscode/c_cpp_properties.json: Removed the entire configuration for C/C++ properties.
    .vscode/launch.json: Removed the Dart launch configurations for different environments and modes.
    .vscode/settings.json: Removed settings related to CMake, spell checking, file nesting, and Dart Flutter SDK path.
    .vscode/snippets.code-snippets: Removed code snippets for Dart, including PaginatedState and PaginatedNotifier templates.
    .vscode/tasks.json: Removed the tasks configuration file.

Documentation Updates:

    CONTRIBUTION.md: Removed heart emoji from the introductory text.
    README.md: Updated the logo image and made minor text adjustments, including removing emojis and updating section titles. [1] [2] [3] [4] [5]

Asset Removals:

    lib/collections/assets.gen.dart: Removed multiple unused asset references, including images related to Spotube logos and banners. [1] [2] [3]

Minor Code Cleanups:

    cli/commands/build/linux.dart, cli/commands/build/windows.dart, cli/commands/translated.dart, cli/commands/untranslated.dart: Adjusted import statements for consistency. [1] [2] [3] [4]
    integration_test/app_test.dart: Removed an unnecessary blank line.
    lib/collections/routes.dart: Commented out the TrackRoute configuration.
2025-04-13 18:40:37 +02:00

282 lines
9.5 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/form/checkbox_form_field.dart';
import 'package:spotube/components/form/text_form_field.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/provider/spotify/spotify.dart';
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);
}
if (trackIds.isNotEmpty) {
await playlistNotifier.addTracks(trackIds, onError);
}
} finally {
isSubmitting.value = false;
if (context.mounted &&
!ref.read(playlistProvider(playlistId ?? "")).hasError) {
context.router.maybePop<Playlist>(
await ref.read(playlistProvider(playlistId ?? "").future),
);
}
}
}
return AlertDialog(
title: Text(
isUpdatingPlaylist
? context.l10n.update_playlist
: context.l10n.create_a_playlist,
),
actions: [
Button.outline(
child: Text(context.l10n.cancel),
onPressed: () {
Navigator.pop(context);
},
),
Button.primary(
onPressed: onCreate,
enabled: !playlist.isLoading & !isSubmitting.value,
child: Text(
isUpdatingPlaylist ? context.l10n.update : context.l10n.create,
),
),
],
content: Container(
width: MediaQuery.of(context).size.width,
constraints: const BoxConstraints(maxWidth: 500),
child: FormBuilder(
key: formKey,
initialValue: {
'playlistName': updatingPlaylist?.name,
'description': updatingPlaylist?.description,
'public': updatingPlaylist?.public ?? false,
'collaborative': updatingPlaylist?.collaborative ?? false,
},
child: ListView(
shrinkWrap: true,
children: [
FormBuilderField<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, SpotifyApiWrapper spotify) {
showDialog(
context: context,
alignment: Alignment.center,
builder: (context) => const ToastLayer(
child: PlaylistCreateDialog(),
),
);
}
@override
Widget build(BuildContext context, ref) {
final spotify = ref.watch(spotifyProvider);
return Button.secondary(
leading: const Icon(SpotubeIcons.addFilled),
child: Text(context.l10n.playlist),
onPressed: () => showPlaylistDialog(context, spotify),
);
}
}