import 'package:flutter/widgets.dart'; enum ShellTab { home, library, repository, settings } class ShellNavigationService { static final homeSearchRequests = ValueNotifier(0); static final chromeBrightness = ValueNotifier(null); static final chromeSurface = ValueNotifier(null); static final _visiblePages = , Route?>{}; static final _chromeScopes = < Object, ({ModalRoute route, Brightness brightness, Color? surface}) >{}; static bool _chromeUpdateScheduled = false; static int _homeSearchGeneration = 0; static void setChromeBrightness({ required Object owner, required ModalRoute route, required Brightness brightness, Color? surface, }) { final scope = (route: route, brightness: brightness, surface: surface); if (_chromeScopes[owner] == scope) return; _chromeScopes[owner] = scope; _scheduleChromeUpdate(); } static void clearChromeBrightness(Object owner) { _chromeScopes.remove(owner); _scheduleChromeUpdate(); } static void _scheduleChromeUpdate() { if (_chromeUpdateScheduled) return; _chromeUpdateScheduled = true; WidgetsBinding.instance.addPostFrameCallback((_) { _chromeUpdateScheduled = false; final activePage = _visiblePages[_activeTabNavigatorKey()]; Brightness? brightness; Color? surface; for (final scope in _chromeScopes.values) { if (identical(scope.route, activePage)) { brightness = scope.brightness; surface = scope.surface; } } chromeBrightness.value = brightness; chromeSurface.value = surface; }); WidgetsBinding.instance.ensureVisualUpdate(); } static void requestHomeSearch() { if (!requestTab(ShellTab.home)) return; final generation = ++_homeSearchGeneration; final owner = _tabSelectionOwner; // PageView creates Home lazily. Deliver the request after its navigator // and focus listener mount, rather than losing the first tap on another tab. WidgetsBinding.instance.addPostFrameCallback((_) { if (generation != _homeSearchGeneration || !identical(owner, _tabSelectionOwner) || _currentTabIndex != 0) { return; } homeTabNavigatorKey.currentState?.popUntil((route) => route.isFirst); homeSearchRequests.value++; // Home scrolls to its search field and requests focus after layout. // Scheduling that frame avoids waiting for another tap to wake it up. WidgetsBinding.instance.ensureVisualUpdate(); }); WidgetsBinding.instance.ensureVisualUpdate(); } static final GlobalKey homeTabNavigatorKey = GlobalKey(); static final GlobalKey libraryTabNavigatorKey = GlobalKey(); static final GlobalKey repoTabNavigatorKey = GlobalKey(); static int _currentTabIndex = 0; static bool _showRepoTab = false; static Object? _tabSelectionOwner; static ValueChanged? _tabSelectionHandler; static void registerTabSelectionHandler({ required Object owner, required ValueChanged handler, }) { _tabSelectionOwner = owner; _tabSelectionHandler = handler; } static void unregisterTabSelectionHandler(Object owner) { if (!identical(_tabSelectionOwner, owner)) return; _tabSelectionOwner = null; _tabSelectionHandler = null; } static bool requestTab(ShellTab tab) { final handler = _tabSelectionHandler; if (handler == null) return false; handler(tab); return true; } static void syncState({ required int currentTabIndex, required bool showRepoTab, }) { if (_currentTabIndex == currentTabIndex && _showRepoTab == showRepoTab) { return; } _currentTabIndex = currentTabIndex; _showRepoTab = showRepoTab; _scheduleChromeUpdate(); } static GlobalKey? _activeTabNavigatorKey() { if (_currentTabIndex == 0) return homeTabNavigatorKey; if (_currentTabIndex == 1) return libraryTabNavigatorKey; if (_showRepoTab && _currentTabIndex == 2) { return repoTabNavigatorKey; } return null; } static NavigatorState? activeTabNavigator() => _activeTabNavigatorKey()?.currentState; } /// Track the top page in each tab. Dialogs keep the underlying page's contrast; /// a pushed album, another tab, or a back gesture restores its own appearance. class ShellChromeObserver extends NavigatorObserver { ShellChromeObserver(this.navigatorKey); final GlobalKey navigatorKey; final _routes = >[]; void _update() { Route? page; for (final route in _routes.reversed) { if (route is PageRoute) { page = route; break; } } ShellNavigationService._visiblePages[navigatorKey] = page; ShellNavigationService._scheduleChromeUpdate(); } @override void didPush(Route route, Route? previousRoute) { _routes.add(route); _update(); } @override void didPop(Route route, Route? previousRoute) { _routes.remove(route); _update(); } @override void didRemove(Route route, Route? previousRoute) { _routes.remove(route); _update(); } @override void didReplace({Route? newRoute, Route? oldRoute}) { final index = oldRoute == null ? -1 : _routes.indexOf(oldRoute); if (index >= 0) { if (newRoute == null) { _routes.removeAt(index); } else { _routes[index] = newRoute; } } _update(); } void detach() { _routes.clear(); ShellNavigationService._visiblePages.remove(navigatorKey); ShellNavigationService._scheduleChromeUpdate(); } }