fix: option picker dialog focus for TV/keyboard nav
This commit is contained in:
@@ -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<T?> showOptionPickerDialog<T>(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required List<({IconData icon, String label, T value})> options,
|
||||
}) {
|
||||
return showDialog<T>(
|
||||
context: context,
|
||||
builder: (context) => _OptionPickerDialog<T>(title: title, options: options),
|
||||
);
|
||||
}
|
||||
|
||||
class _OptionPickerDialog<T> 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<T>> createState() => _OptionPickerDialogState<T>();
|
||||
}
|
||||
|
||||
class _OptionPickerDialogState<T> extends State<_OptionPickerDialog<T>> {
|
||||
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),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<DownloadVersionConfig?> resolveDownloadVersion(
|
||||
/// Show a dialog for selecting a media version.
|
||||
/// Returns the selected index, or null if cancelled.
|
||||
Future<int?> showVersionPickerDialog(BuildContext context, List<PlexMediaVersion> versions, String title) {
|
||||
return showDialog<int>(
|
||||
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<int>(
|
||||
context,
|
||||
title: title,
|
||||
options: List.generate(versions.length, (index) => (
|
||||
icon: Symbols.video_file_rounded,
|
||||
label: versions[index].displayLabel,
|
||||
value: index,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -674,26 +674,13 @@ class MediaContextMenuState extends State<MediaContextMenu> {
|
||||
|
||||
/// Show submenu for Add to... (Playlist or Collection)
|
||||
Future<void> _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<String>(
|
||||
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<String>(
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user