import 'package:flutter/material.dart'; import '../focus/focusable_button.dart'; import '../focus/input_mode_tracker.dart'; import '../i18n/strings.g.dart'; import '../widgets/app_icon.dart'; import '../widgets/focusable_list_tile.dart'; import 'focus_utils.dart'; /// Utility functions for showing common dialogs const _buttonPadding = EdgeInsets.symmetric(horizontal: 18, vertical: 14); const _buttonShape = StadiumBorder(); /// Shows a confirmation dialog with consistent button sizing and autofocus. /// Returns true if user confirmed, false if cancelled. Future showConfirmDialog( BuildContext context, { required String title, required String message, required String confirmText, String? cancelText, bool isDestructive = false, }) async { final confirmed = await showDialog( context: context, builder: (dialogContext) { final colorScheme = Theme.of(dialogContext).colorScheme; return AlertDialog( title: Text(title), content: Text(message), actions: [ FocusableButton( autofocus: true, onPressed: () => Navigator.pop(dialogContext, false), child: TextButton( onPressed: () => Navigator.pop(dialogContext, false), style: TextButton.styleFrom(padding: _buttonPadding, shape: _buttonShape), child: Text(cancelText ?? t.common.cancel), ), ), FocusableButton( onPressed: () => Navigator.pop(dialogContext, true), child: FilledButton( onPressed: () => Navigator.pop(dialogContext, true), style: isDestructive ? FilledButton.styleFrom(backgroundColor: colorScheme.error, foregroundColor: colorScheme.onError) : null, child: Text(confirmText), ), ), ], ); }, ); return confirmed ?? false; } /// Shows a confirmation dialog with an optional checkbox (e.g. "Don't ask again"). /// Returns a record with [confirmed] and [checked] booleans. Future<({bool confirmed, bool checked})> showConfirmDialogWithCheckbox( BuildContext context, { required String title, required String message, required String confirmText, required String checkboxLabel, String? cancelText, }) async { var checked = false; final confirmed = await showDialog( context: context, builder: (dialogContext) { return StatefulBuilder( builder: (context, setDialogState) { return AlertDialog( title: Text(title), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(message), const SizedBox(height: 12), CheckboxListTile( value: checked, onChanged: (v) => setDialogState(() => checked = v ?? false), title: Text(checkboxLabel), contentPadding: EdgeInsets.zero, controlAffinity: ListTileControlAffinity.leading, dense: true, ), ], ), actions: [ FocusableButton( autofocus: true, onPressed: () => Navigator.pop(dialogContext, false), child: TextButton( onPressed: () => Navigator.pop(dialogContext, false), style: TextButton.styleFrom(padding: _buttonPadding, shape: _buttonShape), child: Text(cancelText ?? t.common.cancel), ), ), FocusableButton( onPressed: () => Navigator.pop(dialogContext, true), child: FilledButton( onPressed: () => Navigator.pop(dialogContext, true), child: Text(confirmText), ), ), ], ); }, ); }, ); return (confirmed: confirmed ?? false, checked: checked); } /// Shows a delete confirmation dialog. /// Convenience wrapper around [showConfirmDialog] with destructive styling. Future showDeleteConfirmation(BuildContext context, {required String title, required String message, String? confirmText}) { return showConfirmDialog(context, title: title, message: message, confirmText: confirmText ?? t.common.delete, isDestructive: true); } /// Shows a text input dialog for creating/naming items /// Returns the entered text, or null if cancelled Future showTextInputDialog( BuildContext context, { required String title, required String labelText, required String hintText, String? initialValue, }) { return showDialog( context: context, builder: (context) => _TextInputDialog(title: title, labelText: labelText, hintText: hintText, initialValue: initialValue), ); } /// Shows a multiline text input dialog for editing longer text like summaries. /// Returns the entered text, or null if cancelled. /// Allows empty text to be submitted (for clearing fields). Future showMultilineTextInputDialog( BuildContext context, { required String title, required String labelText, String? initialValue, }) { return showDialog( context: context, builder: (context) => _MultilineTextInputDialog(title: title, labelText: labelText, initialValue: initialValue), ); } class _MultilineTextInputDialog extends StatefulWidget { final String title; final String labelText; final String? initialValue; const _MultilineTextInputDialog({required this.title, required this.labelText, this.initialValue}); @override State<_MultilineTextInputDialog> createState() => _MultilineTextInputDialogState(); } class _MultilineTextInputDialogState extends State<_MultilineTextInputDialog> { late final TextEditingController _controller; final _saveFocusNode = FocusNode(); @override void initState() { super.initState(); _controller = TextEditingController(text: widget.initialValue); } @override void dispose() { _controller.dispose(); _saveFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AlertDialog( title: Text(widget.title), content: SizedBox( width: 400, child: TextField( controller: _controller, autofocus: true, decoration: InputDecoration(labelText: widget.labelText), maxLines: 8, minLines: 3, ), ), actions: [ FocusableButton( onPressed: () => Navigator.pop(context), child: TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)), ), FocusableButton( focusNode: _saveFocusNode, onPressed: () => Navigator.pop(context, _controller.text), child: TextButton( onPressed: () => Navigator.pop(context, _controller.text), child: Text(t.common.save), ), ), ], ); } } class _TextInputDialog extends StatefulWidget { final String title; final String labelText; final String hintText; final String? initialValue; const _TextInputDialog({required this.title, required this.labelText, required this.hintText, this.initialValue}); @override State<_TextInputDialog> createState() => _TextInputDialogState(); } class _TextInputDialogState extends State<_TextInputDialog> { late final TextEditingController _controller; final _saveFocusNode = FocusNode(); @override void initState() { super.initState(); _controller = TextEditingController(text: widget.initialValue); } @override void dispose() { _controller.dispose(); _saveFocusNode.dispose(); super.dispose(); } void _submit() { if (_controller.text.isNotEmpty) { Navigator.pop(context, _controller.text); } } @override Widget build(BuildContext context) { return AlertDialog( title: Text(widget.title), content: TextField( controller: _controller, autofocus: true, decoration: InputDecoration(labelText: widget.labelText, hintText: widget.hintText), textInputAction: TextInputAction.done, onSubmitted: (_) => _saveFocusNode.requestFocus(), ), actions: [ FocusableButton( onPressed: () => Navigator.pop(context), child: TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)), ), FocusableButton( focusNode: _saveFocusNode, onPressed: _submit, child: TextButton(onPressed: _submit, child: Text(t.common.save)), ), ], ); } } /// Shows a simple option picker dialog with focusable items for TV/keyboard navigation. /// Returns the selected value, or null if cancelled. Future showOptionPickerDialog( BuildContext context, { required String title, required List<({IconData icon, String label, T value})> options, }) { final focusFirstItem = InputModeTracker.isKeyboardMode(context); return showDialog( context: context, builder: (context) => _OptionPickerDialog(title: title, options: options, focusFirstItem: focusFirstItem), ); } class _OptionPickerDialog extends StatefulWidget { final String title; final List<({IconData icon, String label, T value})> options; final bool focusFirstItem; const _OptionPickerDialog({required this.title, required this.options, this.focusFirstItem = false}); @override State<_OptionPickerDialog> createState() => _OptionPickerDialogState(); } class _OptionPickerDialogState extends State<_OptionPickerDialog> { late final FocusNode _initialFocusNode; @override void initState() { super.initState(); _initialFocusNode = FocusNode(debugLabel: 'OptionPickerInitialFocus'); if (widget.focusFirstItem) { FocusUtils.requestFocusAfterBuild(this, _initialFocusNode); } } @override void dispose() { _initialFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return SimpleDialog( title: Text(widget.title), contentPadding: const EdgeInsets.symmetric(vertical: 8), children: List.generate(widget.options.length, (index) { final option = widget.options[index]; return FocusableListTile( focusNode: index == 0 && widget.focusFirstItem ? _initialFocusNode : null, 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), ); }), ); } }