mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-20 22:41:38 +02:00
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:
1 parent
485be2f468
commit
fe29f304aa
4 files changed
+106
-140
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user