From e88d9e03905552dd57f7f2eb5319e7a59a477bb2 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 26 Jan 2026 02:01:23 +0100 Subject: [PATCH] fix: dpad nav to libraries action buttons close #335 --- lib/screens/libraries/libraries_screen.dart | 131 ++++++++++++++++++-- 1 file changed, 122 insertions(+), 9 deletions(-) diff --git a/lib/screens/libraries/libraries_screen.dart b/lib/screens/libraries/libraries_screen.dart index 4bab2240..04ff705c 100644 --- a/lib/screens/libraries/libraries_screen.dart +++ b/lib/screens/libraries/libraries_screen.dart @@ -115,6 +115,12 @@ class _LibrariesScreenState extends State final _collectionsTabChipFocusNode = FocusNode(debugLabel: 'tab_chip_collections'); final _playlistsTabChipFocusNode = FocusNode(debugLabel: 'tab_chip_playlists'); + // App bar action button focus + late FocusNode _editButtonFocusNode; + late FocusNode _refreshButtonFocusNode; + bool _isEditFocused = false; + bool _isRefreshFocused = false; + // Scroll controller for the outer CustomScrollView final ScrollController _outerScrollController = ScrollController(); @@ -124,6 +130,12 @@ class _LibrariesScreenState extends State _tabController = TabController(length: 4, vsync: this); _tabController.addListener(_onTabChanged); + // Initialize action button focus nodes + _editButtonFocusNode = FocusNode(debugLabel: 'EditButton'); + _refreshButtonFocusNode = FocusNode(debugLabel: 'RefreshButton'); + _editButtonFocusNode.addListener(_onEditFocusChange); + _refreshButtonFocusNode.addListener(_onRefreshFocusChange); + // Initialize with libraries from the provider WidgetsBinding.instance.addPostFrameCallback((_) { _initializeWithLibraries(); @@ -370,6 +382,75 @@ class _LibrariesScreenState extends State focusScope?.focusSidebar(); } + void _onEditFocusChange() { + if (mounted) { + setState(() => _isEditFocused = _editButtonFocusNode.hasFocus); + } + } + + void _onRefreshFocusChange() { + if (mounted) { + setState(() => _isRefreshFocused = _refreshButtonFocusNode.hasFocus); + } + } + + /// Handle key events for the edit button in app bar + KeyEventResult _handleEditKeyEvent(FocusNode node, KeyEvent event) { + if (!event.isActionable) return KeyEventResult.ignored; + final key = event.logicalKey; + + if (key.isLeftKey) { + // Navigate back to last tab (Playlists) + _getTabChipFocusNode(3).requestFocus(); + return KeyEventResult.handled; + } + if (key.isRightKey) { + _refreshButtonFocusNode.requestFocus(); + return KeyEventResult.handled; + } + if (key.isDownKey) { + _focusCurrentTab(); + return KeyEventResult.handled; + } + if (key.isUpKey) { + return KeyEventResult.handled; // Block at boundary + } + if (key.isSelectKey) { + _showLibraryManagementSheet(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } + + /// Handle key events for the refresh button in app bar + KeyEventResult _handleRefreshKeyEvent(FocusNode node, KeyEvent event) { + if (!event.isActionable) return KeyEventResult.ignored; + final key = event.logicalKey; + + if (key.isLeftKey) { + // Navigate to edit button if libraries exist, else to last tab + final librariesProvider = context.read(); + if (librariesProvider.libraries.isNotEmpty) { + _editButtonFocusNode.requestFocus(); + } else { + _getTabChipFocusNode(3).requestFocus(); + } + return KeyEventResult.handled; + } + if (key.isRightKey || key.isUpKey) { + return KeyEventResult.handled; // Block at boundary + } + if (key.isDownKey) { + _focusCurrentTab(); + return KeyEventResult.handled; + } + if (key.isSelectKey) { + _refreshCurrentTab(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } + @override void dispose() { _tabController.removeListener(_onTabChanged); @@ -380,6 +461,10 @@ class _LibrariesScreenState extends State _browseTabChipFocusNode.dispose(); _collectionsTabChipFocusNode.dispose(); _playlistsTabChipFocusNode.dispose(); + _editButtonFocusNode.removeListener(_onEditFocusChange); + _editButtonFocusNode.dispose(); + _refreshButtonFocusNode.removeListener(_onRefreshFocusChange); + _refreshButtonFocusNode.dispose(); // Clear L1/R1 callbacks GamepadService.onL1Pressed = null; GamepadService.onR1Pressed = null; @@ -955,7 +1040,15 @@ class _LibrariesScreenState extends State }); _getTabChipFocusNode(newIndex).requestFocus(); } - : null, + : () { + // Navigate to first action button (edit if libraries exist, else refresh) + final librariesProvider = context.read(); + if (librariesProvider.libraries.isNotEmpty) { + _editButtonFocusNode.requestFocus(); + } else { + _refreshButtonFocusNode.requestFocus(); + } + }, onNavigateDown: _focusCurrentTabFromTabBar, onBack: _onTabBarBack, ); @@ -1060,15 +1153,35 @@ class _LibrariesScreenState extends State scrolledUnderElevation: 0, actions: [ if (allLibraries.isNotEmpty) - IconButton( - icon: const AppIcon(Symbols.edit_rounded, fill: 1), - tooltip: t.libraries.manageLibraries, - onPressed: _showLibraryManagementSheet, + Focus( + focusNode: _editButtonFocusNode, + onKeyEvent: _handleEditKeyEvent, + child: Container( + decoration: BoxDecoration( + color: _isEditFocused ? Theme.of(context).colorScheme.surfaceContainerHighest : Colors.transparent, + borderRadius: BorderRadius.circular(20), + ), + child: IconButton( + icon: const AppIcon(Symbols.edit_rounded, fill: 1), + tooltip: t.libraries.manageLibraries, + onPressed: _showLibraryManagementSheet, + ), + ), + ), + Focus( + focusNode: _refreshButtonFocusNode, + onKeyEvent: _handleRefreshKeyEvent, + child: Container( + decoration: BoxDecoration( + color: _isRefreshFocused ? Theme.of(context).colorScheme.surfaceContainerHighest : Colors.transparent, + borderRadius: BorderRadius.circular(20), + ), + child: IconButton( + icon: const AppIcon(Symbols.refresh_rounded, fill: 1), + tooltip: t.common.refresh, + onPressed: _refreshCurrentTab, + ), ), - IconButton( - icon: const AppIcon(Symbols.refresh_rounded, fill: 1), - tooltip: t.common.refresh, - onPressed: _refreshCurrentTab, ), ], ),