refactor(playlists): share the playlist name prompt and dispose its controller

The create, picker and rename dialogs were three copies of one form. They
now call promptPlaylistName, whose controller lives in a dialog state and is
disposed with it.
This commit is contained in:
zarzet committed 2026-10-19 00:48:31 +07:00
1 parent 485be2f468
commit fe29f304aa
4 files changed
+106 -140

No files matched your search

+5 -45
View File
@@ -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<FormState>();
final nextName = await showAppDialog<String>(
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) {
+1 -1
View File
@@ -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;
+4 -43
View File
@@ -306,49 +306,10 @@ extension _QueueTabNavigation on _QueueTabState {
}
Future<void> _showCreatePlaylistDialog(BuildContext context) async {
final controller = TextEditingController();
final formKey = GlobalKey<FormState>();
final playlistName = await showAppDialog<String>(
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;
+96 -51
View File
@@ -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<String?> _promptPlaylistName(
BuildContext context,
String? playlistNamePrefill,
) async {
final controller = TextEditingController(text: playlistNamePrefill);
final formKey = GlobalKey<FormState>();
final result = await showAppDialog<String>(
/// Asks for a non-empty playlist name and returns it trimmed, or null when
/// the dialog is cancelled.
Future<String?> promptPlaylistName(
BuildContext context, {
required String title,
required String confirmLabel,
String? initialName,
TextInputAction? textInputAction,
bool isDefault = false,
}) {
return showAppDialog<String>(
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<FormState>();
@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 {