feat: Add natural D-pad navigation for hub sections
- Add left navigation from hubs to sidebar when at leftmost item - Add up navigation from first hub to tab bar (Libraries) or hero section (Discover) - Enhance HubSection with onNavigateLeft and onNavigateUp callbacks - Update _handleVerticalNavigation to return false at top boundary for proper UP handling - Implement _navigateToSidebar() in Library Recommended tab and Discover screen - Update all HubSection instances with proper navigation callbacks
This commit is contained in:
@@ -28,6 +28,7 @@ import '../mixins/watch_state_aware.dart';
|
||||
import '../utils/watch_state_notifier.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../utils/provider_extensions.dart';
|
||||
import 'main_screen.dart';
|
||||
import '../utils/video_player_navigation.dart';
|
||||
import '../utils/layout_constants.dart';
|
||||
import '../utils/platform_detector.dart';
|
||||
@@ -178,6 +179,12 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Navigate to the sidebar (called when user presses left at leftmost item)
|
||||
void _navigateToSidebar() {
|
||||
final focusScope = MainScreenFocusScope.of(context);
|
||||
focusScope?.focusSidebar();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -1033,6 +1040,11 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
onRemoveFromContinueWatching: _refreshContinueWatching,
|
||||
isInContinueWatching: true,
|
||||
onVerticalNavigation: (isUp) => _handleVerticalNavigation(0, isUp),
|
||||
onNavigateLeft: _navigateToSidebar,
|
||||
onNavigateUp: () {
|
||||
_heroFocusNode.requestFocus();
|
||||
_scrollController.animateTo(0, duration: const Duration(milliseconds: 200), curve: Curves.easeOut);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1047,6 +1059,11 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
onRefresh: updateItem,
|
||||
// Hub index is i + 1 if continue watching exists, otherwise i
|
||||
onVerticalNavigation: (isUp) => _handleVerticalNavigation(_onDeck.isNotEmpty ? i + 1 : i, isUp),
|
||||
onNavigateLeft: _navigateToSidebar,
|
||||
onNavigateUp: (i == 0 && _onDeck.isEmpty) ? () {
|
||||
_heroFocusNode.requestFocus();
|
||||
_scrollController.animateTo(0, duration: const Duration(milliseconds: 200), curve: Curves.easeOut);
|
||||
} : null,
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../../../mixins/item_updatable.dart';
|
||||
import '../../../models/plex_hub.dart';
|
||||
import '../../../models/plex_metadata.dart';
|
||||
import '../../../widgets/hub_section.dart';
|
||||
import '../../main_screen.dart';
|
||||
import 'base_library_tab.dart';
|
||||
|
||||
/// Recommended tab for library screen
|
||||
@@ -115,8 +116,13 @@ class _LibraryRecommendedTabState extends BaseLibraryTabState<PlexHub, LibraryRe
|
||||
final targetIndex = isUp ? hubIndex - 1 : hubIndex + 1;
|
||||
|
||||
// Check if target is valid
|
||||
if (targetIndex < 0 || targetIndex >= _hubKeys.length) {
|
||||
// At boundary, block navigation
|
||||
if (targetIndex < 0) {
|
||||
// At top boundary - return false to allow onNavigateUp to handle it
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetIndex >= _hubKeys.length) {
|
||||
// At bottom boundary, block navigation
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -130,6 +136,12 @@ class _LibraryRecommendedTabState extends BaseLibraryTabState<PlexHub, LibraryRe
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Navigate to the sidebar (called when user presses left at leftmost item)
|
||||
void _navigateToSidebar() {
|
||||
final focusScope = MainScreenFocusScope.of(context);
|
||||
focusScope?.focusSidebar();
|
||||
}
|
||||
|
||||
/// Focus the first item in the first hub (for tab activation)
|
||||
@override
|
||||
void focusFirstItem() {
|
||||
@@ -158,6 +170,8 @@ class _LibraryRecommendedTabState extends BaseLibraryTabState<PlexHub, LibraryRe
|
||||
onRemoveFromContinueWatching: isContinueWatching ? _refreshContinueWatching : null,
|
||||
onVerticalNavigation: (isUp) => _handleVerticalNavigation(index, isUp),
|
||||
onBack: widget.onBack,
|
||||
onNavigateLeft: _navigateToSidebar,
|
||||
onNavigateUp: index == 0 ? widget.onBack : null,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -39,6 +39,14 @@ class HubSection extends StatefulWidget {
|
||||
/// Used to navigate focus back to the tab bar.
|
||||
final VoidCallback? onBack;
|
||||
|
||||
/// Called when the user presses LEFT while at the leftmost item.
|
||||
/// Used to navigate focus to the side navigation rail.
|
||||
final VoidCallback? onNavigateLeft;
|
||||
|
||||
/// Called when the user presses UP while at the topmost item (first hub).
|
||||
/// Used to navigate focus to the tab bar.
|
||||
final VoidCallback? onNavigateUp;
|
||||
|
||||
const HubSection({
|
||||
super.key,
|
||||
required this.hub,
|
||||
@@ -49,6 +57,8 @@ class HubSection extends StatefulWidget {
|
||||
this.showServerName = false,
|
||||
this.onVerticalNavigation,
|
||||
this.onBack,
|
||||
this.onNavigateLeft,
|
||||
this.onNavigateUp,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -172,6 +182,9 @@ class HubSectionState extends State<HubSection> {
|
||||
HubFocusMemory.setForHub(widget.hub.hubKey, _focusedIndex);
|
||||
_scrollToIndex(_focusedIndex);
|
||||
setState(() {});
|
||||
} else if (widget.onNavigateLeft != null) {
|
||||
// At leftmost item - navigate to sidebar
|
||||
widget.onNavigateLeft!();
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
@@ -189,7 +202,11 @@ class HubSectionState extends State<HubSection> {
|
||||
|
||||
// Up/Down: delegate to parent for vertical hub navigation, ALWAYS consume
|
||||
if (key.isUpKey) {
|
||||
widget.onVerticalNavigation?.call(true);
|
||||
final handled = widget.onVerticalNavigation?.call(true) ?? false;
|
||||
// If not handled (at top boundary) and we have onNavigateUp, call it
|
||||
if (!handled && widget.onNavigateUp != null) {
|
||||
widget.onNavigateUp!();
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key.isDownKey) {
|
||||
|
||||
Reference in New Issue
Block a user