diff --git a/lib/screens/library_tracks_folder_screen.dart b/lib/screens/library_tracks_folder_screen.dart index 418b3ba7..7eee59b1 100644 --- a/lib/screens/library_tracks_folder_screen.dart +++ b/lib/screens/library_tracks_folder_screen.dart @@ -4,14 +4,12 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as p; import 'package:spotiflac_android/widgets/app_icon_button.dart'; -import 'package:spotiflac_android/widgets/app_text_field.dart'; import 'package:spotiflac_android/widgets/collection_scaffold.dart'; import 'package:spotiflac_android/widgets/expressive_button.dart'; import 'package:spotiflac_android/widgets/expressive_button_group.dart'; import 'package:spotiflac_android/theme/app_tokens.dart'; import 'package:spotiflac_android/widgets/track_card.dart'; import 'package:spotiflac_android/widgets/app_bottom_sheet.dart'; -import 'package:spotiflac_android/widgets/app_alert_dialog.dart'; import 'package:spotiflac_android/widgets/app_snack_bar.dart'; import 'package:spotiflac_android/theme/mornye_theme.dart'; import 'package:spotiflac_android/theme/mornye_icons.dart'; @@ -818,49 +816,11 @@ class _LibraryTracksFolderScreenState String playlistId, String currentName, ) async { - final controller = TextEditingController(text: currentName); - final formKey = GlobalKey(); - - final nextName = await showAppDialog( - context: context, - builder: (dialogContext) { - return AppAlertDialog( - title: Text(dialogContext.l10n.collectionRenamePlaylist), - content: Form( - key: formKey, - child: AppTextField( - controller: controller, - autofocus: true, - hint: dialogContext.l10n.collectionPlaylistNameHint, - validator: (value) { - final trimmed = value?.trim() ?? ''; - if (trimmed.isEmpty) { - return dialogContext.l10n.collectionPlaylistNameRequired; - } - return null; - }, - onSubmitted: (_) { - if (formKey.currentState?.validate() != true) return; - Navigator.of(dialogContext).pop(controller.text.trim()); - }, - ), - ), - actions: [ - AppDialogAction( - onPressed: () => Navigator.of(dialogContext).pop(), - child: Text(dialogContext.l10n.dialogCancel), - ), - AppDialogAction( - filled: true, - onPressed: () { - if (formKey.currentState?.validate() != true) return; - Navigator.of(dialogContext).pop(controller.text.trim()); - }, - child: Text(dialogContext.l10n.dialogSave), - ), - ], - ); - }, + final nextName = await promptPlaylistName( + context, + title: context.l10n.collectionRenamePlaylist, + confirmLabel: context.l10n.dialogSave, + initialName: currentName, ); if (nextName == null || nextName.trim().isEmpty || !context.mounted) { diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index eedb9401..f2ae25f7 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:spotiflac_android/widgets/app_alert_dialog.dart'; import 'package:spotiflac_android/widgets/app_choice_chip.dart'; import 'package:spotiflac_android/widgets/app_content_card.dart'; -import 'package:spotiflac_android/widgets/app_text_field.dart'; import 'package:spotiflac_android/services/local_track_batch_actions.dart'; import 'package:spotiflac_android/services/shell_navigation_service.dart'; import 'package:spotiflac_android/widgets/error_card.dart'; @@ -31,6 +30,7 @@ import 'package:spotiflac_android/widgets/library_selection_playback_actions.dar import 'package:spotiflac_android/widgets/local_library_artwork_builder.dart'; import 'package:spotiflac_android/widgets/library_playlist_drag_source.dart'; import 'package:spotiflac_android/widgets/mornye_chrome.dart'; +import 'package:spotiflac_android/widgets/playlist_picker_sheet.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart' show DateFormat; diff --git a/lib/screens/queue_tab_navigation.dart b/lib/screens/queue_tab_navigation.dart index 976bc8d3..13c631cc 100644 --- a/lib/screens/queue_tab_navigation.dart +++ b/lib/screens/queue_tab_navigation.dart @@ -306,49 +306,10 @@ extension _QueueTabNavigation on _QueueTabState { } Future _showCreatePlaylistDialog(BuildContext context) async { - final controller = TextEditingController(); - final formKey = GlobalKey(); - - final playlistName = await showAppDialog( - context: context, - builder: (dialogContext) { - return AppAlertDialog( - title: Text(dialogContext.l10n.collectionCreatePlaylist), - content: Form( - key: formKey, - child: AppTextField( - controller: controller, - autofocus: true, - hint: dialogContext.l10n.collectionPlaylistNameHint, - validator: (value) { - final trimmed = value?.trim() ?? ''; - if (trimmed.isEmpty) { - return dialogContext.l10n.collectionPlaylistNameRequired; - } - return null; - }, - onSubmitted: (_) { - if (formKey.currentState?.validate() != true) return; - Navigator.of(dialogContext).pop(controller.text.trim()); - }, - ), - ), - actions: [ - AppDialogAction( - onPressed: () => Navigator.of(dialogContext).pop(), - child: Text(dialogContext.l10n.dialogCancel), - ), - AppDialogAction( - filled: true, - onPressed: () { - if (formKey.currentState?.validate() != true) return; - Navigator.of(dialogContext).pop(controller.text.trim()); - }, - child: Text(dialogContext.l10n.actionCreate), - ), - ], - ); - }, + final playlistName = await promptPlaylistName( + context, + title: context.l10n.collectionCreatePlaylist, + confirmLabel: context.l10n.actionCreate, ); if (playlistName == null || playlistName.isEmpty) return; diff --git a/lib/widgets/playlist_picker_sheet.dart b/lib/widgets/playlist_picker_sheet.dart index 72f30313..6a94a408 100644 --- a/lib/widgets/playlist_picker_sheet.dart +++ b/lib/widgets/playlist_picker_sheet.dart @@ -225,9 +225,13 @@ class _PlaylistPickerSheetContentState enabled: !_isSubmitting, onTap: () async { if (_isSubmitting) return; - final name = await _promptPlaylistName( + final name = await promptPlaylistName( context, - widget.playlistNamePrefill, + title: context.l10n.collectionCreatePlaylist, + confirmLabel: context.l10n.actionCreate, + initialName: widget.playlistNamePrefill, + textInputAction: TextInputAction.done, + isDefault: true, ); if (name == null || name.trim().isEmpty || !context.mounted) { return; @@ -349,58 +353,99 @@ class _PlaylistPickerSheetContentState } } -Future _promptPlaylistName( - BuildContext context, - String? playlistNamePrefill, -) async { - final controller = TextEditingController(text: playlistNamePrefill); - final formKey = GlobalKey(); - - final result = await showAppDialog( +/// Asks for a non-empty playlist name and returns it trimmed, or null when +/// the dialog is cancelled. +Future promptPlaylistName( + BuildContext context, { + required String title, + required String confirmLabel, + String? initialName, + TextInputAction? textInputAction, + bool isDefault = false, +}) { + return showAppDialog( context: context, - builder: (dialogContext) { - return AppAlertDialog( - title: Text(dialogContext.l10n.collectionCreatePlaylist), - content: Form( - key: formKey, - child: AppTextField( - controller: controller, - autofocus: true, - textInputAction: TextInputAction.done, - hint: dialogContext.l10n.collectionPlaylistNameHint, - validator: (value) { - final trimmed = value?.trim() ?? ''; - if (trimmed.isEmpty) { - return dialogContext.l10n.collectionPlaylistNameRequired; - } - return null; - }, - onSubmitted: (_) { - if (formKey.currentState?.validate() != true) return; - Navigator.of(dialogContext).pop(controller.text.trim()); - }, - ), - ), - actions: [ - AppDialogAction( - onPressed: () => Navigator.of(dialogContext).pop(), - child: Text(dialogContext.l10n.dialogCancel), - ), - AppDialogAction( - filled: true, - isDefault: true, - onPressed: () { - if (formKey.currentState?.validate() != true) return; - Navigator.of(dialogContext).pop(controller.text.trim()); - }, - child: Text(dialogContext.l10n.actionCreate), - ), - ], - ); - }, + builder: (_) => _PlaylistNameDialog( + title: title, + confirmLabel: confirmLabel, + initialName: initialName, + textInputAction: textInputAction, + isDefault: isDefault, + ), ); +} - return result; +class _PlaylistNameDialog extends StatefulWidget { + const _PlaylistNameDialog({ + required this.title, + required this.confirmLabel, + required this.initialName, + required this.textInputAction, + required this.isDefault, + }); + + final String title; + final String confirmLabel; + final String? initialName; + final TextInputAction? textInputAction; + final bool isDefault; + + @override + State<_PlaylistNameDialog> createState() => _PlaylistNameDialogState(); +} + +class _PlaylistNameDialogState extends State<_PlaylistNameDialog> { + late final TextEditingController _controller = TextEditingController( + text: widget.initialName, + ); + final _formKey = GlobalKey(); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _submit() { + if (_formKey.currentState?.validate() != true) return; + Navigator.of(context).pop(_controller.text.trim()); + } + + @override + Widget build(BuildContext context) { + return AppAlertDialog( + title: Text(widget.title), + content: Form( + key: _formKey, + child: AppTextField( + controller: _controller, + autofocus: true, + textInputAction: widget.textInputAction, + hint: context.l10n.collectionPlaylistNameHint, + validator: (value) { + final trimmed = value?.trim() ?? ''; + if (trimmed.isEmpty) { + return context.l10n.collectionPlaylistNameRequired; + } + return null; + }, + onSubmitted: (_) => _submit(), + ), + ), + actions: [ + AppDialogAction( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.dialogCancel), + ), + AppDialogAction( + filled: true, + isDefault: widget.isDefault, + onPressed: _submit, + child: Text(widget.confirmLabel), + ), + ], + ); + } } class _PlaylistPickerRow extends StatelessWidget {