diff --git a/lib/utils/dialogs.dart b/lib/utils/dialogs.dart index fc6a6205..919be326 100644 --- a/lib/utils/dialogs.dart +++ b/lib/utils/dialogs.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import '../focus/focusable_button.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 @@ -270,3 +273,61 @@ class _TextInputDialogState extends State<_TextInputDialog> { ); } } + +/// 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, +}) { + return showDialog( + context: context, + builder: (context) => _OptionPickerDialog(title: title, options: options), + ); +} + +class _OptionPickerDialog extends StatefulWidget { + final String title; + final List<({IconData icon, String label, T value})> options; + + const _OptionPickerDialog({required this.title, required this.options}); + + @override + State<_OptionPickerDialog> createState() => _OptionPickerDialogState(); +} + +class _OptionPickerDialogState extends State<_OptionPickerDialog> { + late final FocusNode _initialFocusNode; + + @override + void initState() { + super.initState(); + _initialFocusNode = FocusNode(debugLabel: 'OptionPickerInitialFocus'); + 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 ? _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), + ); + }), + ); + } +} diff --git a/lib/utils/download_version_utils.dart b/lib/utils/download_version_utils.dart index c7d71cef..330f1209 100644 --- a/lib/utils/download_version_utils.dart +++ b/lib/utils/download_version_utils.dart @@ -4,7 +4,7 @@ import '../models/plex_media_version.dart'; import '../models/plex_metadata.dart'; import '../services/plex_client.dart'; import '../utils/app_logger.dart'; -import '../widgets/app_icon.dart'; +import '../utils/dialogs.dart'; import '../i18n/strings.g.dart'; /// Configuration for download version selection, threaded through the queue pipeline. @@ -81,26 +81,14 @@ Future resolveDownloadVersion( /// Show a dialog for selecting a media version. /// Returns the selected index, or null if cancelled. Future showVersionPickerDialog(BuildContext context, List versions, String title) { - return showDialog( - context: context, - builder: (dialogContext) => SimpleDialog( - title: Text(title), - contentPadding: const EdgeInsets.symmetric(vertical: 8), - children: List.generate(versions.length, (index) { - final version = versions[index]; - return SimpleDialogOption( - onPressed: () => Navigator.pop(dialogContext, index), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - child: Row( - children: [ - AppIcon(Symbols.video_file_rounded, fill: 1, size: 24), - const SizedBox(width: 16), - Text(version.displayLabel, style: Theme.of(dialogContext).textTheme.bodyLarge), - ], - ), - ); - }), - ), + return showOptionPickerDialog( + context, + title: title, + options: List.generate(versions.length, (index) => ( + icon: Symbols.video_file_rounded, + label: versions[index].displayLabel, + value: index, + )), ); } diff --git a/lib/widgets/media_context_menu.dart b/lib/widgets/media_context_menu.dart index 8bade493..4fa6e7b9 100644 --- a/lib/widgets/media_context_menu.dart +++ b/lib/widgets/media_context_menu.dart @@ -674,26 +674,13 @@ class MediaContextMenuState extends State { /// Show submenu for Add to... (Playlist or Collection) Future _showAddToSubmenu(BuildContext context) async { - final submenuActions = [ - _MenuAction(value: 'playlist', icon: Symbols.playlist_play_rounded, label: t.playlists.playlist), - _MenuAction(value: 'collection', icon: Symbols.collections_rounded, label: t.collections.collection), - ]; - - final selected = await showDialog( - context: context, - builder: (dialogContext) => SimpleDialog( - title: Text(t.common.addTo), - contentPadding: const EdgeInsets.symmetric(vertical: 8), - children: submenuActions.map((action) { - return SimpleDialogOption( - onPressed: () => Navigator.pop(dialogContext, action.value), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - child: Row( - children: [AppIcon(action.icon, fill: 1, size: 24), const SizedBox(width: 16), Text(action.label, style: Theme.of(dialogContext).textTheme.bodyLarge)], - ), - ); - }).toList(), - ), + final selected = await showOptionPickerDialog( + context, + title: t.common.addTo, + options: [ + (icon: Symbols.playlist_play_rounded, label: t.playlists.playlist, value: 'playlist'), + (icon: Symbols.collections_rounded, label: t.collections.collection, value: 'collection'), + ], ); // Handle the submenu selection