Files
plezy/lib/screens/main_screen.dart
T

325 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../../services/plex_client.dart';
import '../i18n/strings.g.dart';
import '../utils/app_logger.dart';
import '../utils/keyboard_utils.dart';
import '../utils/provider_extensions.dart';
import '../main.dart';
import '../mixins/refreshable.dart';
import '../providers/multi_server_provider.dart';
import '../providers/server_state_provider.dart';
import '../providers/hidden_libraries_provider.dart';
import '../providers/playback_state_provider.dart';
import '../services/plex_auth_service.dart';
import '../services/storage_service.dart';
import 'discover_screen.dart';
import 'libraries_screen.dart';
import 'search_screen.dart';
import 'settings_screen.dart';
/// InheritedWidget that provides back navigation functionality to child screens
class BackNavigationScope extends InheritedWidget {
final VoidCallback focusBottomNav;
const BackNavigationScope({
super.key,
required this.focusBottomNav,
required super.child,
});
static BackNavigationScope? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<BackNavigationScope>();
}
static BackNavigationScope? maybeOf(BuildContext context) {
return context.getInheritedWidgetOfExactType<BackNavigationScope>();
}
@override
bool updateShouldNotify(BackNavigationScope oldWidget) {
return focusBottomNav != oldWidget.focusBottomNav;
}
}
class MainScreen extends StatefulWidget {
final PlexClient client;
const MainScreen({super.key, required this.client});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> with RouteAware {
int _currentIndex = 0;
late final List<Widget> _screens;
final GlobalKey<State<DiscoverScreen>> _discoverKey = GlobalKey();
final GlobalKey<State<LibrariesScreen>> _librariesKey = GlobalKey();
final GlobalKey<State<SearchScreen>> _searchKey = GlobalKey();
final GlobalKey<State<SettingsScreen>> _settingsKey = GlobalKey();
/// Focus scope node for the bottom navigation bar
/// Using FocusScopeNode so requestFocus() focuses the first child
late final FocusScopeNode _bottomNavFocusScopeNode;
/// Focus scope node for the main content area
late final FocusScopeNode _contentFocusScopeNode;
@override
void initState() {
super.initState();
_bottomNavFocusScopeNode = FocusScopeNode(debugLabel: 'BottomNavigation');
_contentFocusScopeNode = FocusScopeNode(debugLabel: 'MainContent');
_screens = [
DiscoverScreen(
key: _discoverKey,
onBecameVisible: _onDiscoverBecameVisible,
),
LibrariesScreen(key: _librariesKey),
SearchScreen(key: _searchKey),
SettingsScreen(key: _settingsKey),
];
// Set up data invalidation callback for profile switching
WidgetsBinding.instance.addPostFrameCallback((_) async {
// Initialize UserProfileProvider to ensure it's ready after sign-in
final userProfileProvider = context.userProfile;
await userProfileProvider.initialize();
// Set up data invalidation callback for profile switching
userProfileProvider.setDataInvalidationCallback(_invalidateAllScreens);
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
}
@override
void dispose() {
routeObserver.unsubscribe(this);
_bottomNavFocusScopeNode.dispose();
_contentFocusScopeNode.dispose();
super.dispose();
}
/// Focus the bottom navigation bar (called by child screens on back press)
void _focusBottomNav() {
// Request focus on the scope, then navigate to the currently selected tab
_bottomNavFocusScopeNode.requestFocus();
WidgetsBinding.instance.addPostFrameCallback((_) {
// Move to first item, then advance to current index
_bottomNavFocusScopeNode.nextFocus();
for (int i = 0; i < _currentIndex; i++) {
_bottomNavFocusScopeNode.nextFocus();
}
});
}
/// Focus the content area (called when back is pressed in navbar)
void _focusContent() {
_contentFocusScopeNode.requestFocus();
}
/// Handle back key in navbar - focus content area
KeyEventResult _handleNavBarBackKey(FocusNode node, KeyEvent event) {
if (isBackKeyEvent(event)) {
_focusContent();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
@override
void didPush() {
// Called when this route has been pushed (initial navigation)
if (_currentIndex == 0) {
_onDiscoverBecameVisible();
}
}
@override
void didPopNext() {
// Called when returning to this route from a child route (e.g., from video player)
if (_currentIndex == 0) {
_onDiscoverBecameVisible();
}
}
void _onDiscoverBecameVisible() {
appLogger.d('Navigated to home');
// Refresh content when returning to discover page
final discoverState = _discoverKey.currentState;
if (discoverState != null && discoverState is Refreshable) {
(discoverState as Refreshable).refresh();
}
}
/// Invalidate all cached data across all screens when profile is switched
/// Receives the list of servers with new profile tokens for reconnection
Future<void> _invalidateAllScreens(List<PlexServer> servers) async {
appLogger.d(
'Invalidating all screen data due to profile switch with ${servers.length} servers',
);
// Get all providers
final multiServerProvider = context.read<MultiServerProvider>();
final serverStateProvider = context.read<ServerStateProvider>();
final hiddenLibrariesProvider = context.read<HiddenLibrariesProvider>();
final playbackStateProvider = context.read<PlaybackStateProvider>();
// Reconnect to all servers with new profile tokens
if (servers.isNotEmpty) {
final storage = await StorageService.getInstance();
final clientId = storage.getClientIdentifier();
final connectedCount = await multiServerProvider.reconnectWithServers(
servers,
clientIdentifier: clientId,
);
appLogger.d(
'Reconnected to $connectedCount/${servers.length} servers after profile switch',
);
}
// Reset other provider states
serverStateProvider.reset();
hiddenLibrariesProvider.refresh();
playbackStateProvider.clearShuffle();
appLogger.d('Cleared all provider states for profile switch');
// Full refresh discover screen (reload all content for new profile)
final discoverState = _discoverKey.currentState;
if (discoverState != null) {
(discoverState as dynamic).fullRefresh();
}
// Full refresh libraries screen (clear filters and reload for new profile)
final librariesState = _librariesKey.currentState;
if (librariesState != null) {
(librariesState as dynamic).fullRefresh();
}
// Full refresh search screen (clear search for new profile)
final searchState = _searchKey.currentState;
if (searchState != null) {
(searchState as dynamic).fullRefresh();
}
}
void _selectTab(int index) {
// Check if selection came from keyboard/d-pad (bottom nav has focus)
final isKeyboardNavigation = _bottomNavFocusScopeNode.hasFocus;
setState(() {
_currentIndex = index;
});
// Notify discover screen when it becomes visible via tab switch
if (index == 0) {
_onDiscoverBecameVisible();
// Focus hero when selecting Home tab via keyboard/d-pad
if (isKeyboardNavigation) {
final discoverState = _discoverKey.currentState;
if (discoverState != null) {
(discoverState as dynamic).focusHero();
}
}
}
// Focus first content item when selecting Libraries tab via keyboard/d-pad
if (index == 1 && isKeyboardNavigation) {
final librariesState = _librariesKey.currentState;
if (librariesState != null) {
(librariesState as dynamic).focusFirstContentItem();
}
}
// Focus search input when selecting Search tab (for both click/tap and keyboard)
if (index == 2) {
final searchState = _searchKey.currentState;
if (searchState != null) {
(searchState as dynamic).focusSearchInput();
}
}
// Move focus to the content area when selecting a tab via keyboard
if (isKeyboardNavigation) {
_contentFocusScopeNode.requestFocus();
}
}
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: {
// Number keys 1-4 for quick tab switching
const SingleActivator(LogicalKeyboardKey.digit1): _TabIntent(0),
const SingleActivator(LogicalKeyboardKey.digit2): _TabIntent(1),
const SingleActivator(LogicalKeyboardKey.digit3): _TabIntent(2),
const SingleActivator(LogicalKeyboardKey.digit4): _TabIntent(3),
},
child: Actions(
actions: {
_TabIntent: CallbackAction<_TabIntent>(
onInvoke: (intent) {
_selectTab(intent.tabIndex);
return null;
},
),
},
child: Scaffold(
body: BackNavigationScope(
focusBottomNav: _focusBottomNav,
child: FocusScope(
node: _contentFocusScopeNode,
child: FocusTraversalGroup(
child: IndexedStack(index: _currentIndex, children: _screens),
),
),
),
bottomNavigationBar: FocusScope(
node: _bottomNavFocusScopeNode,
onKeyEvent: _handleNavBarBackKey,
child: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: _selectTab,
destinations: [
NavigationDestination(
icon: const Icon(Icons.home_outlined),
selectedIcon: const Icon(Icons.home),
label: t.navigation.home,
),
NavigationDestination(
icon: const Icon(Icons.video_library_outlined),
selectedIcon: const Icon(Icons.video_library),
label: t.navigation.libraries,
),
NavigationDestination(
icon: const Icon(Icons.search),
selectedIcon: const Icon(Icons.search),
label: t.navigation.search,
),
NavigationDestination(
icon: const Icon(Icons.settings_outlined),
selectedIcon: const Icon(Icons.settings),
label: t.navigation.settings,
),
],
),
),
),
),
);
}
}
/// Intent for switching tabs via keyboard shortcuts
class _TabIntent extends Intent {
final int tabIndex;
const _TabIntent(this.tabIndex);
}