diff --git a/lib/utils/dialogs.dart b/lib/utils/dialogs.dart index 5097e1dc..8bd29ecc 100644 --- a/lib/utils/dialogs.dart +++ b/lib/utils/dialogs.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import '../focus/focusable_button.dart'; import '../focus/input_mode_tracker.dart'; import '../i18n/strings.g.dart'; @@ -131,11 +132,23 @@ Future showTextInputDialog( required String labelText, required String hintText, String? initialValue, + String? confirmText, + TextInputType? keyboardType, + List? inputFormatters, + String? Function(String)? validator, }) { return showDialog( context: context, - builder: (context) => - _TextInputDialog(title: title, labelText: labelText, hintText: hintText, initialValue: initialValue), + builder: (context) => _TextInputDialog( + title: title, + labelText: labelText, + hintText: hintText, + initialValue: initialValue, + confirmText: confirmText, + keyboardType: keyboardType, + inputFormatters: inputFormatters, + validator: validator, + ), ); } @@ -219,8 +232,21 @@ class _TextInputDialog extends StatefulWidget { final String labelText; final String hintText; final String? initialValue; + final String? confirmText; + final TextInputType? keyboardType; + final List? inputFormatters; + final String? Function(String)? validator; - const _TextInputDialog({required this.title, required this.labelText, required this.hintText, this.initialValue}); + const _TextInputDialog({ + required this.title, + required this.labelText, + required this.hintText, + this.initialValue, + this.confirmText, + this.keyboardType, + this.inputFormatters, + this.validator, + }); @override State<_TextInputDialog> createState() => _TextInputDialogState(); @@ -244,9 +270,10 @@ class _TextInputDialogState extends State<_TextInputDialog> { } void _submit() { - if (_controller.text.isNotEmpty) { - Navigator.pop(context, _controller.text); - } + final text = _controller.text; + if (text.isEmpty) return; + if (widget.validator != null && widget.validator!(text) != null) return; + Navigator.pop(context, text); } @override @@ -257,6 +284,8 @@ class _TextInputDialogState extends State<_TextInputDialog> { controller: _controller, autofocus: true, decoration: InputDecoration(labelText: widget.labelText, hintText: widget.hintText), + keyboardType: widget.keyboardType, + inputFormatters: widget.inputFormatters, textInputAction: TextInputAction.done, onSubmitted: (_) => _saveFocusNode.requestFocus(), ), @@ -268,7 +297,7 @@ class _TextInputDialogState extends State<_TextInputDialog> { FocusableButton( focusNode: _saveFocusNode, onPressed: _submit, - child: TextButton(onPressed: _submit, child: Text(t.common.save)), + child: TextButton(onPressed: _submit, child: Text(widget.confirmText ?? t.common.save)), ), ], ); @@ -281,11 +310,17 @@ Future showOptionPickerDialog( BuildContext context, { required String title, required List<({IconData icon, String label, T value})> options, + Future Function(T value)? onBeforeClose, }) { final focusFirstItem = InputModeTracker.isKeyboardMode(context); return showDialog( context: context, - builder: (context) => _OptionPickerDialog(title: title, options: options, focusFirstItem: focusFirstItem), + builder: (context) => _OptionPickerDialog( + title: title, + options: options, + focusFirstItem: focusFirstItem, + onBeforeClose: onBeforeClose, + ), ); } @@ -293,8 +328,9 @@ class _OptionPickerDialog extends StatefulWidget { final String title; final List<({IconData icon, String label, T value})> options; final bool focusFirstItem; + final Future Function(T value)? onBeforeClose; - const _OptionPickerDialog({required this.title, required this.options, this.focusFirstItem = false}); + const _OptionPickerDialog({required this.title, required this.options, this.focusFirstItem = false, this.onBeforeClose}); @override State<_OptionPickerDialog> createState() => _OptionPickerDialogState(); @@ -330,7 +366,14 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> { leading: AppIcon(option.icon, fill: 1, size: 24), title: Text(option.label, style: Theme.of(context).textTheme.bodyLarge), contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), - onTap: () => Navigator.pop(context, option.value), + onTap: () async { + if (widget.onBeforeClose != null) { + final result = await widget.onBeforeClose!(option.value); + if (context.mounted) Navigator.pop(context, result); + } else { + Navigator.pop(context, option.value); + } + }, ); }), ); diff --git a/lib/utils/download_utils.dart b/lib/utils/download_utils.dart index f19608c9..9f9c701e 100644 --- a/lib/utils/download_utils.dart +++ b/lib/utils/download_utils.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:material_symbols_icons/symbols.dart'; -import '../focus/focusable_button.dart'; -import '../focus/input_mode_tracker.dart'; import '../i18n/strings.g.dart'; import '../models/plex_metadata.dart'; import '../providers/download_provider.dart'; @@ -28,6 +26,7 @@ Future showDownloadOptionsAndQueue( int? maxCount; if (mt == PlexMediaType.show || mt == PlexMediaType.season) { + int? customCount; final selected = await showOptionPickerDialog<_DownloadChoice>( context, title: t.downloads.downloadNow, @@ -38,6 +37,11 @@ Future showDownloadOptionsAndQueue( (icon: Symbols.filter_9_plus_rounded, label: t.downloads.nextNUnwatched(count: 10), value: _DownloadChoice.next10), (icon: Symbols.tune_rounded, label: t.downloads.customAmount, value: _DownloadChoice.custom), ], + onBeforeClose: (value) async { + if (value != _DownloadChoice.custom) return value; + customCount = await _showEpisodeCountDialog(context); + return customCount != null ? value : null; + }, ); if (selected == null || !context.mounted) return null; @@ -54,10 +58,8 @@ Future showDownloadOptionsAndQueue( filter = DownloadFilter.unwatched; maxCount = 10; case _DownloadChoice.custom: - final count = await _showEpisodeCountDialog(context); - if (count == null || !context.mounted) return null; filter = DownloadFilter.unwatched; - maxCount = count; + maxCount = customCount; } } @@ -101,69 +103,21 @@ Future showPlaylistDownloadOptionsAndQueue( ); } -Future _showEpisodeCountDialog(BuildContext context) { - final autoFocus = InputModeTracker.isKeyboardMode(context); - return showDialog( - context: context, - builder: (context) => _EpisodeCountDialog(autoFocus: autoFocus), +Future _showEpisodeCountDialog(BuildContext context) async { + final result = await showTextInputDialog( + context, + title: t.downloads.howManyEpisodes, + labelText: '', + hintText: '', + confirmText: t.common.ok, + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + validator: (text) { + final n = int.tryParse(text); + if (n == null || n <= 0) return ''; + return null; + }, ); -} - -class _EpisodeCountDialog extends StatefulWidget { - final bool autoFocus; - const _EpisodeCountDialog({this.autoFocus = false}); - - @override - State<_EpisodeCountDialog> createState() => _EpisodeCountDialogState(); -} - -class _EpisodeCountDialogState extends State<_EpisodeCountDialog> { - late final TextEditingController _controller; - final _submitFocusNode = FocusNode(); - - @override - void initState() { - super.initState(); - _controller = TextEditingController(); - } - - @override - void dispose() { - _controller.dispose(); - _submitFocusNode.dispose(); - super.dispose(); - } - - void _submit() { - final count = int.tryParse(_controller.text); - if (count != null && count > 0) { - Navigator.pop(context, count); - } - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Text(t.downloads.howManyEpisodes), - content: TextField( - controller: _controller, - autofocus: widget.autoFocus, - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - textInputAction: TextInputAction.done, - onSubmitted: (_) => _submitFocusNode.requestFocus(), - ), - actions: [ - FocusableButton( - onPressed: () => Navigator.pop(context), - child: TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)), - ), - FocusableButton( - focusNode: _submitFocusNode, - onPressed: _submit, - child: TextButton(onPressed: _submit, child: Text(t.common.ok)), - ), - ], - ); - } + if (result == null) return null; + return int.tryParse(result); }