312 lines
8.7 KiB
Dart
312 lines
8.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'route_paths.dart';
|
|
import 'route_guards.dart';
|
|
|
|
/// Navigation shell with bottom navigation or navigation drawer
|
|
class NavigationShell extends ConsumerStatefulWidget {
|
|
final Widget child;
|
|
final GoRouterState state;
|
|
|
|
const NavigationShell({
|
|
super.key,
|
|
required this.child,
|
|
required this.state,
|
|
});
|
|
|
|
@override
|
|
ConsumerState<NavigationShell> createState() => _NavigationShellState();
|
|
}
|
|
|
|
class _NavigationShellState extends ConsumerState<NavigationShell> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final authState = ref.watch(authStateProvider);
|
|
final currentPath = widget.state.fullPath ?? '/';
|
|
|
|
// Determine if we should show bottom navigation
|
|
final showBottomNav = _shouldShowBottomNavigation(currentPath, authState);
|
|
|
|
if (!showBottomNav) {
|
|
return widget.child;
|
|
}
|
|
|
|
// Get current navigation index
|
|
final currentIndex = _getCurrentNavigationIndex(currentPath);
|
|
|
|
return Scaffold(
|
|
body: widget.child,
|
|
bottomNavigationBar: NavigationBar(
|
|
selectedIndex: currentIndex,
|
|
onDestinationSelected: (index) => _onNavigationTapped(context, index),
|
|
destinations: _getNavigationDestinations(authState),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Determine if bottom navigation should be shown
|
|
bool _shouldShowBottomNavigation(String path, AuthState authState) {
|
|
// Don't show on auth pages
|
|
if (RoutePaths.isAuthPath(path)) {
|
|
return false;
|
|
}
|
|
|
|
// Don't show on onboarding
|
|
if (path.startsWith(RoutePaths.onboarding) || path.startsWith(RoutePaths.welcome)) {
|
|
return false;
|
|
}
|
|
|
|
// Don't show on error pages
|
|
if (path.startsWith(RoutePaths.error) || path.startsWith(RoutePaths.notFound)) {
|
|
return false;
|
|
}
|
|
|
|
// Don't show on specific detail pages
|
|
final hideOnPaths = [
|
|
'/todos/add',
|
|
'/todos/',
|
|
'/settings/',
|
|
];
|
|
|
|
for (final hidePath in hideOnPaths) {
|
|
if (path.contains(hidePath) && path != RoutePaths.todos && path != RoutePaths.settings) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// Get current navigation index based on path
|
|
int _getCurrentNavigationIndex(String path) {
|
|
if (path.startsWith(RoutePaths.todos)) {
|
|
return 1;
|
|
} else if (path.startsWith(RoutePaths.settings)) {
|
|
return 2;
|
|
} else if (path.startsWith(RoutePaths.profile)) {
|
|
return 3;
|
|
}
|
|
return 0; // Home
|
|
}
|
|
|
|
/// Get navigation destinations based on auth state
|
|
List<NavigationDestination> _getNavigationDestinations(AuthState authState) {
|
|
return [
|
|
const NavigationDestination(
|
|
icon: Icon(Icons.home_outlined),
|
|
selectedIcon: Icon(Icons.home),
|
|
label: 'Home',
|
|
),
|
|
const NavigationDestination(
|
|
icon: Icon(Icons.check_circle_outline),
|
|
selectedIcon: Icon(Icons.check_circle),
|
|
label: 'Todos',
|
|
),
|
|
const NavigationDestination(
|
|
icon: Icon(Icons.settings_outlined),
|
|
selectedIcon: Icon(Icons.settings),
|
|
label: 'Settings',
|
|
),
|
|
if (authState == AuthState.authenticated)
|
|
const NavigationDestination(
|
|
icon: Icon(Icons.person_outline),
|
|
selectedIcon: Icon(Icons.person),
|
|
label: 'Profile',
|
|
),
|
|
];
|
|
}
|
|
|
|
/// Handle navigation tap
|
|
void _onNavigationTapped(BuildContext context, int index) {
|
|
final authState = ref.read(authStateProvider);
|
|
|
|
switch (index) {
|
|
case 0:
|
|
if (GoRouterState.of(context).fullPath != RoutePaths.home) {
|
|
context.go(RoutePaths.home);
|
|
}
|
|
break;
|
|
case 1:
|
|
if (!GoRouterState.of(context).fullPath!.startsWith(RoutePaths.todos)) {
|
|
context.go(RoutePaths.todos);
|
|
}
|
|
break;
|
|
case 2:
|
|
if (!GoRouterState.of(context).fullPath!.startsWith(RoutePaths.settings)) {
|
|
context.go(RoutePaths.settings);
|
|
}
|
|
break;
|
|
case 3:
|
|
if (authState == AuthState.authenticated) {
|
|
if (GoRouterState.of(context).fullPath != RoutePaths.profile) {
|
|
context.go(RoutePaths.profile);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Adaptive navigation shell that changes based on screen size
|
|
class AdaptiveNavigationShell extends ConsumerWidget {
|
|
final Widget child;
|
|
final GoRouterState state;
|
|
|
|
const AdaptiveNavigationShell({
|
|
super.key,
|
|
required this.child,
|
|
required this.state,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final screenWidth = MediaQuery.of(context).size.width;
|
|
|
|
// Use rail navigation on tablets and desktop
|
|
if (screenWidth >= 840) {
|
|
return _NavigationRailShell(
|
|
child: child,
|
|
state: state,
|
|
);
|
|
}
|
|
|
|
// Use bottom navigation on mobile
|
|
return NavigationShell(
|
|
child: child,
|
|
state: state,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Navigation rail for larger screens
|
|
class _NavigationRailShell extends ConsumerStatefulWidget {
|
|
final Widget child;
|
|
final GoRouterState state;
|
|
|
|
const _NavigationRailShell({
|
|
required this.child,
|
|
required this.state,
|
|
});
|
|
|
|
@override
|
|
ConsumerState<_NavigationRailShell> createState() => _NavigationRailShellState();
|
|
}
|
|
|
|
class _NavigationRailShellState extends ConsumerState<_NavigationRailShell> {
|
|
bool isExtended = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final authState = ref.watch(authStateProvider);
|
|
final currentPath = widget.state.fullPath ?? '/';
|
|
|
|
// Determine if we should show navigation rail
|
|
final showNavRail = _shouldShowNavigationRail(currentPath, authState);
|
|
|
|
if (!showNavRail) {
|
|
return widget.child;
|
|
}
|
|
|
|
// Get current navigation index
|
|
final currentIndex = _getCurrentNavigationIndex(currentPath);
|
|
|
|
return Scaffold(
|
|
body: Row(
|
|
children: [
|
|
NavigationRail(
|
|
extended: isExtended,
|
|
selectedIndex: currentIndex,
|
|
onDestinationSelected: (index) => _onNavigationTapped(context, index),
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.menu),
|
|
onPressed: () {
|
|
setState(() {
|
|
isExtended = !isExtended;
|
|
});
|
|
},
|
|
),
|
|
destinations: _getNavigationDestinations(authState),
|
|
),
|
|
const VerticalDivider(thickness: 1, width: 1),
|
|
Expanded(child: widget.child),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
bool _shouldShowNavigationRail(String path, AuthState authState) {
|
|
// Same logic as bottom navigation
|
|
if (RoutePaths.isAuthPath(path)) return false;
|
|
if (path.startsWith(RoutePaths.onboarding) || path.startsWith(RoutePaths.welcome)) return false;
|
|
if (path.startsWith(RoutePaths.error) || path.startsWith(RoutePaths.notFound)) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
int _getCurrentNavigationIndex(String path) {
|
|
if (path.startsWith(RoutePaths.todos)) {
|
|
return 1;
|
|
} else if (path.startsWith(RoutePaths.settings)) {
|
|
return 2;
|
|
} else if (path.startsWith(RoutePaths.profile)) {
|
|
return 3;
|
|
}
|
|
return 0; // Home
|
|
}
|
|
|
|
List<NavigationRailDestination> _getNavigationDestinations(AuthState authState) {
|
|
return [
|
|
const NavigationRailDestination(
|
|
icon: Icon(Icons.home_outlined),
|
|
selectedIcon: Icon(Icons.home),
|
|
label: Text('Home'),
|
|
),
|
|
const NavigationRailDestination(
|
|
icon: Icon(Icons.check_circle_outline),
|
|
selectedIcon: Icon(Icons.check_circle),
|
|
label: Text('Todos'),
|
|
),
|
|
const NavigationRailDestination(
|
|
icon: Icon(Icons.settings_outlined),
|
|
selectedIcon: Icon(Icons.settings),
|
|
label: Text('Settings'),
|
|
),
|
|
if (authState == AuthState.authenticated)
|
|
const NavigationRailDestination(
|
|
icon: Icon(Icons.person_outline),
|
|
selectedIcon: Icon(Icons.person),
|
|
label: Text('Profile'),
|
|
),
|
|
];
|
|
}
|
|
|
|
void _onNavigationTapped(BuildContext context, int index) {
|
|
final authState = ref.read(authStateProvider);
|
|
|
|
switch (index) {
|
|
case 0:
|
|
if (GoRouterState.of(context).fullPath != RoutePaths.home) {
|
|
context.go(RoutePaths.home);
|
|
}
|
|
break;
|
|
case 1:
|
|
if (!GoRouterState.of(context).fullPath!.startsWith(RoutePaths.todos)) {
|
|
context.go(RoutePaths.todos);
|
|
}
|
|
break;
|
|
case 2:
|
|
if (!GoRouterState.of(context).fullPath!.startsWith(RoutePaths.settings)) {
|
|
context.go(RoutePaths.settings);
|
|
}
|
|
break;
|
|
case 3:
|
|
if (authState == AuthState.authenticated) {
|
|
if (GoRouterState.of(context).fullPath != RoutePaths.profile) {
|
|
context.go(RoutePaths.profile);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
} |