refactor: reuse showTextInputDialog for episode count input
This commit is contained in:
+53
-10
@@ -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<String?> showTextInputDialog(
|
||||
required String labelText,
|
||||
required String hintText,
|
||||
String? initialValue,
|
||||
String? confirmText,
|
||||
TextInputType? keyboardType,
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
String? Function(String)? validator,
|
||||
}) {
|
||||
return showDialog<String>(
|
||||
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<TextInputFormatter>? 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<T?> showOptionPickerDialog<T>(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required List<({IconData icon, String label, T value})> options,
|
||||
Future<T?> Function(T value)? onBeforeClose,
|
||||
}) {
|
||||
final focusFirstItem = InputModeTracker.isKeyboardMode(context);
|
||||
return showDialog<T>(
|
||||
context: context,
|
||||
builder: (context) => _OptionPickerDialog<T>(title: title, options: options, focusFirstItem: focusFirstItem),
|
||||
builder: (context) => _OptionPickerDialog<T>(
|
||||
title: title,
|
||||
options: options,
|
||||
focusFirstItem: focusFirstItem,
|
||||
onBeforeClose: onBeforeClose,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -293,8 +328,9 @@ class _OptionPickerDialog<T> extends StatefulWidget {
|
||||
final String title;
|
||||
final List<({IconData icon, String label, T value})> options;
|
||||
final bool focusFirstItem;
|
||||
final Future<T?> 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<T>> createState() => _OptionPickerDialogState<T>();
|
||||
@@ -330,7 +366,14 @@ class _OptionPickerDialogState<T> extends State<_OptionPickerDialog<T>> {
|
||||
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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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<int?> 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<int?> 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<int?> 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<int?> showPlaylistDownloadOptionsAndQueue(
|
||||
);
|
||||
}
|
||||
|
||||
Future<int?> _showEpisodeCountDialog(BuildContext context) {
|
||||
final autoFocus = InputModeTracker.isKeyboardMode(context);
|
||||
return showDialog<int>(
|
||||
context: context,
|
||||
builder: (context) => _EpisodeCountDialog(autoFocus: autoFocus),
|
||||
Future<int?> _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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user