refactor: extract FocusableActionBar widget for reusable app bar actions

Replace duplicated focusable action button pattern across 8 screens with
a shared FocusableActionBar widget that handles focus nodes, D-pad
navigation, back key, and focus decoration automatically.
This commit is contained in:
edde746
2026-02-27 11:55:19 +01:00
parent 8559037a50
commit cc30bee83a
9 changed files with 407 additions and 681 deletions
+12 -43
View File
@@ -17,7 +17,7 @@ import '../widgets/desktop_app_bar.dart';
import '../widgets/overlay_sheet.dart';
import 'package:flutter/services.dart';
import '../focus/dpad_navigator.dart';
import '../focus/focus_theme.dart';
import '../focus/focusable_action_bar.dart';
import '../focus/input_mode_tracker.dart';
import '../focus/key_event_utils.dart';
import '../mixins/grid_focus_node_mixin.dart';
@@ -48,7 +48,7 @@ class _HubDetailScreenState extends State<HubDetailScreen> with Refreshable, Gri
String? _errorMessage;
late final FocusNode _firstItemFocusNode = FocusNode(debugLabel: 'hub_detail_first_item');
late final FocusNode _sortButtonFocusNode = FocusNode(debugLabel: 'hub_detail_sort');
final _actionBarKey = GlobalKey<FocusableActionBarState>();
bool _isAppBarFocused = false;
bool _backHandledByKeyEvent = false;
@@ -63,7 +63,6 @@ class _HubDetailScreenState extends State<HubDetailScreen> with Refreshable, Gri
@override
void initState() {
super.initState();
_sortButtonFocusNode.addListener(_onSortButtonFocusChange);
// Start with items already loaded in the hub
_items = widget.hub.items;
_filteredItems = widget.hub.items;
@@ -84,23 +83,11 @@ class _HubDetailScreenState extends State<HubDetailScreen> with Refreshable, Gri
@override
void dispose() {
_sortButtonFocusNode.removeListener(_onSortButtonFocusChange);
_firstItemFocusNode.dispose();
_sortButtonFocusNode.dispose();
disposeGridFocusNodes();
super.dispose();
}
void _onSortButtonFocusChange() {
if (!mounted) return;
final hasFocus = _sortButtonFocusNode.hasFocus;
if (hasFocus && !_isAppBarFocused) {
setState(() => _isAppBarFocused = true);
} else if (!hasFocus && _isAppBarFocused) {
setState(() => _isAppBarFocused = false);
}
}
void _focusGrid() {
if (_filteredItems.isEmpty) return;
final targetIndex =
@@ -114,7 +101,7 @@ class _HubDetailScreenState extends State<HubDetailScreen> with Refreshable, Gri
void _navigateToAppBar() {
setState(() => _isAppBarFocused = true);
_sortButtonFocusNode.requestFocus();
_actionBarKey.currentState?.getFocusNode(0).requestFocus();
}
void _handleBackFromContent() {
@@ -122,24 +109,6 @@ class _HubDetailScreenState extends State<HubDetailScreen> with Refreshable, Gri
_navigateToAppBar();
}
KeyEventResult _handleSortButtonKeyEvent(FocusNode _, KeyEvent event) {
final key = event.logicalKey;
final backResult = handleBackKeyAction(event, () => Navigator.pop(context));
if (backResult != KeyEventResult.ignored) return backResult;
if (event is! KeyDownEvent) return KeyEventResult.ignored;
if (key.isDownKey) {
_focusGrid();
return KeyEventResult.handled;
}
if (key.isSelectKey) {
_showSortBottomSheet();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
Future<void> _loadSorts() async {
try {
@@ -315,7 +284,6 @@ class _HubDetailScreenState extends State<HubDetailScreen> with Refreshable, Gri
@override
Widget build(BuildContext context) {
final isKeyboardMode = InputModeTracker.isKeyboardMode(context);
final sortButtonFocused = isKeyboardMode && _isAppBarFocused;
return PopScope(
canPop: !isKeyboardMode || _isAppBarFocused,
@@ -336,16 +304,17 @@ class _HubDetailScreenState extends State<HubDetailScreen> with Refreshable, Gri
title: Text(widget.hub.title),
pinned: true,
actions: [
Focus(
focusNode: _sortButtonFocusNode,
onKeyEvent: _handleSortButtonKeyEvent,
child: Container(
decoration: FocusTheme.focusBackgroundDecoration(isFocused: sortButtonFocused, borderRadius: 20),
child: IconButton(
icon: AppIcon(Symbols.swap_vert_rounded, fill: 1, semanticLabel: t.libraries.sort),
FocusableActionBar(
key: _actionBarKey,
onNavigateDown: _focusGrid,
onBack: () => Navigator.pop(context),
actions: [
FocusableAction(
icon: Symbols.swap_vert_rounded,
tooltip: t.libraries.sort,
onPressed: _showSortBottomSheet,
),
),
],
),
],
),