diff --git a/lib/mixins/tab_visibility_aware.dart b/lib/mixins/tab_visibility_aware.dart index a136ed67..28a4308b 100644 --- a/lib/mixins/tab_visibility_aware.dart +++ b/lib/mixins/tab_visibility_aware.dart @@ -3,6 +3,6 @@ /// Used by MainScreen to pause expensive work (e.g. animation tickers) /// when the screen's tab is no longer visible, and resume it when shown again. mixin TabVisibilityAware { - void onTabShown({bool scrollToTop = true}); + void onTabShown(); void onTabHidden(); } diff --git a/lib/screens/discover_screen.dart b/lib/screens/discover_screen.dart index 25330081..e7126118 100644 --- a/lib/screens/discover_screen.dart +++ b/lib/screens/discover_screen.dart @@ -57,7 +57,7 @@ class DiscoverScreen extends StatefulWidget { } class _DiscoverScreenState extends State - with Refreshable, FullRefreshable, ItemUpdatable, WatchStateAware, TabVisibilityAware, WidgetsBindingObserver { + with Refreshable, FullRefreshable, ItemUpdatable, WatchStateAware, TabVisibilityAware, FocusableTab, WidgetsBindingObserver { static const Duration _heroAutoScrollDuration = Duration(seconds: 8); static const Duration _indicatorUpdateInterval = Duration(milliseconds: 200); @@ -395,13 +395,15 @@ class _DiscoverScreenState extends State } @override - void onTabShown({bool scrollToTop = true}) { + void onTabShown() { if (!_isAutoScrollPaused) { _startAutoScroll(); } - if (scrollToTop) { - _focusTopBoundary(); - } + } + + @override + void focusActiveTabIfReady() { + _focusTopBoundary(); } // Helper method to calculate visible dot range (max 5 dots) diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 65001578..d0bb6b2a 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -675,61 +675,15 @@ class _MainScreenState extends State with RouteAware, WindowListener void _focusContent() { setState(() => _isSidebarFocused = false); _contentFocusScope.requestFocus(); - // When content regains focus while on Discover, focus the hero section - if (_currentIndex == 0 && !_isOffline) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_discoverKey.currentState case final TabVisibilityAware aware) { - aware.onTabShown(); - } - }); - } - // When content regains focus while on Libraries, only focus the active tab - // if no child already has focus (avoids stale focus requests stealing focus - // from the current item when scrolling back to item 0) - if (_currentIndex == 1 && !_isOffline) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_contentFocusScope.focusedChild == null) { - if (_librariesKey.currentState case final FocusableTab focusable) { - focusable.focusActiveTabIfReady(); - } - } - }); - } - // When content regains focus while on Live TV, focus the active guide/whats-on tab - final liveTvIndex = NavigationTab.indexFor(NavigationTabId.liveTv, isOffline: _isOffline, hasLiveTv: _hasLiveTv); - if (_currentIndex == liveTvIndex && liveTvIndex >= 0) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_liveTvKey.currentState case final FocusableTab focusable) { + // Only programmatically focus if the scope didn't auto-restore a child. + // This preserves the user's focus position when returning from sidebar. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_contentFocusScope.focusedChild == null) { + if (_screenKeyForIndex(_currentIndex)?.currentState case final FocusableTab focusable) { focusable.focusActiveTabIfReady(); } - }); - } - // When content regains focus while on Settings, restore focus to last focused setting - final settingsIndex = NavigationTab.indexFor( - NavigationTabId.settings, - isOffline: _isOffline, - hasLiveTv: _hasLiveTv, - ); - if (_currentIndex == settingsIndex) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_settingsKey.currentState case final FocusableTab focusable) { - focusable.focusActiveTabIfReady(); - } - }); - } - // When content regains focus while on Search, focus the search input - final searchIndex = NavigationTab.indexFor( - NavigationTabId.search, - isOffline: _isOffline, - hasLiveTv: _hasLiveTv, - ); - if (_currentIndex == searchIndex && searchIndex >= 0) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_searchKey.currentState case final SearchInputFocusable searchable) { - searchable.focusSearchInput(); - } - }); - } + } + }); } /// Suppress stray back events after a child route pops. @@ -834,7 +788,7 @@ class _MainScreenState extends State with RouteAware, WindowListener // Called when returning to this route from a child route (e.g., from video player) if (_currentIndex == 0 && !_isOffline) { if (_discoverKey.currentState case final TabVisibilityAware aware) { - aware.onTabShown(scrollToTop: false); + aware.onTabShown(); } _onDiscoverBecameVisible(); } @@ -922,56 +876,24 @@ class _MainScreenState extends State with RouteAware, WindowListener } }); - // Handle screen-specific logic - final settingsIndex = NavigationTab.indexFor( - NavigationTabId.settings, - isOffline: _isOffline, - hasLiveTv: _hasLiveTv, - ); - - // Skip online-only screen logic in offline mode - if (!_isOffline) { - // Pause/resume discover auto-scroll when switching tabs - if (previousIndex == 0 && index != 0) { - if (_discoverKey.currentState case final TabVisibilityAware aware) { - aware.onTabHidden(); - } + if (previousIndex != index) { + // Notify previous screen it's being hidden + if (_screenKeyForIndex(previousIndex)?.currentState case final TabVisibilityAware aware) { + aware.onTabHidden(); } - // Notify discover screen when it becomes visible via tab switch - if (index == 0) { - if (previousIndex != 0) { - if (_discoverKey.currentState case final TabVisibilityAware aware) { - aware.onTabShown(); - } - } - _onDiscoverBecameVisible(); + // Notify and focus new screen + final newState = _screenKeyForIndex(index)?.currentState; + if (newState case final TabVisibilityAware aware) { + aware.onTabShown(); } - // Ensure the libraries screen applies focus when brought into view - if (index == 1 && previousIndex != 1) { - if (_librariesKey.currentState case final FocusableTab focusable) { - focusable.focusActiveTabIfReady(); - } - } - // Ensure the Live TV screen applies focus when brought into view - final liveTvIdx = NavigationTab.indexFor(NavigationTabId.liveTv, isOffline: _isOffline, hasLiveTv: _hasLiveTv); - if (index == liveTvIdx && liveTvIdx >= 0 && previousIndex != liveTvIdx) { - if (_liveTvKey.currentState case final FocusableTab focusable) { - focusable.focusActiveTabIfReady(); - } - } - // Focus search input when selecting Search tab - if (NavigationTab.isTabAtIndex(NavigationTabId.search, index, isOffline: _isOffline, hasLiveTv: _hasLiveTv)) { - if (_searchKey.currentState case final SearchInputFocusable searchable) { - searchable.focusSearchInput(); - } + if (newState case final FocusableTab focusable) { + focusable.focusActiveTabIfReady(); } } - // Restore focus when switching to Settings tab (works in both online and offline mode) - if (index == settingsIndex && previousIndex != settingsIndex) { - if (_settingsKey.currentState case final FocusableTab focusable) { - focusable.focusActiveTabIfReady(); - } + // Discover: always refresh content (even on re-selection) + if (!_isOffline && _tabIdForIndex(_isOffline, index) == NavigationTabId.discover) { + _onDiscoverBecameVisible(); } } @@ -1011,6 +933,18 @@ class _MainScreenState extends State with RouteAware, WindowListener return tabs[safeIndex].id; } + /// Get the GlobalKey for the screen at the given tab index. + GlobalKey? _screenKeyForIndex(int index) { + return switch (_tabIdForIndex(_isOffline, index)) { + NavigationTabId.discover => _discoverKey, + NavigationTabId.libraries => _librariesKey, + NavigationTabId.liveTv => _liveTvKey, + NavigationTabId.search => _searchKey, + NavigationTabId.downloads => _downloadsKey, + NavigationTabId.settings => _settingsKey, + }; + } + /// Build navigation destinations for bottom navigation bar. List _buildNavDestinations(bool isOffline) { return _getVisibleTabs(isOffline).map((tab) => tab.toDestination()).toList(); diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 0c5b8902..a86d28d5 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -24,7 +24,7 @@ class SearchScreen extends StatefulWidget { State createState() => _SearchScreenState(); } -class _SearchScreenState extends State with Refreshable, FullRefreshable, SearchInputFocusable { +class _SearchScreenState extends State with Refreshable, FullRefreshable, SearchInputFocusable, FocusableTab { final _searchController = TextEditingController(); final _searchFocusNode = FocusNode(debugLabel: 'SearchInput'); final _firstResultFocusNode = FocusNode(debugLabel: 'SearchFirstResult'); @@ -129,6 +129,11 @@ class _SearchScreenState extends State with Refreshable, FullRefre _searchFocusNode.requestFocus(); } + @override + void focusActiveTabIfReady() { + _searchFocusNode.requestFocus(); + } + /// Set the search query externally (e.g. from companion remote) @override void setSearchQuery(String query) {