import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../auth/auth_provider.dart'; import '../../features/auth/login_screen.dart'; import '../../features/auth/register_screen.dart'; import '../../features/home/home_screen.dart'; import '../../features/onboarding/onboarding_screen.dart'; import '../../features/profile/profile_provider.dart'; import '../../shared/models/user.dart'; import '../../features/products/products_screen.dart'; import '../../features/products/add_product_screen.dart'; import '../../features/scan/scan_screen.dart'; import '../../features/scan/recognition_confirm_screen.dart'; import '../../features/scan/recognition_service.dart'; import '../../features/menu/menu_screen.dart'; import '../../features/menu/shopping_list_screen.dart'; import '../../features/recipes/recipe_detail_screen.dart'; import '../../features/recipes/recipes_screen.dart'; import '../../features/profile/profile_screen.dart'; import '../../features/products/product_provider.dart'; import '../../features/scan/recognition_history_screen.dart'; import '../../shared/models/recipe.dart'; import '../../shared/models/saved_recipe.dart'; // Notifies GoRouter when auth state or profile state changes. class _RouterNotifier extends ChangeNotifier { _RouterNotifier(Ref ref) { ref.listen(authProvider, (previous, next) { // Reload profile whenever auth transitions to authenticated. // This handles the case where profileProvider did an initial load before // tokens were available (401 → AsyncError) and needs a fresh load after login. if (next.status == AuthStatus.authenticated && previous?.status != AuthStatus.authenticated) { ref.read(profileProvider.notifier).load(); } notifyListeners(); }); ref.listen>(profileProvider, (_, __) => notifyListeners()); } } final routerProvider = Provider((ref) { final notifier = _RouterNotifier(ref); ref.onDispose(notifier.dispose); return GoRouter( initialLocation: '/loading', refreshListenable: notifier, redirect: (context, state) { // Use ref.read — this runs inside GoRouter, not inside a Riverpod build. final authState = ref.read(authProvider); final isLoggedIn = authState.status == AuthStatus.authenticated; final isAuthRoute = state.matchedLocation.startsWith('/auth'); final isOnboarding = state.matchedLocation.startsWith('/onboarding'); // Show splash until the stored-token check completes. if (authState.status == AuthStatus.unknown) return '/loading'; if (!isLoggedIn && !isAuthRoute) return '/auth/login'; if (isLoggedIn) { // Reading profileProvider triggers its lazy initialization (load() in constructor). final profileState = ref.read(profileProvider); // Keep showing splash while profile loads. if (profileState.isLoading) { return state.matchedLocation == '/loading' ? null : '/loading'; } final profileUser = profileState.valueOrNull; // If profile failed to load, don't block navigation. if (profileUser == null) { if (isAuthRoute || state.matchedLocation == '/loading') return '/home'; return null; } final needsOnboarding = !profileUser.hasCompletedOnboarding; if (isAuthRoute || state.matchedLocation == '/loading') { return needsOnboarding ? '/onboarding' : '/home'; } if (needsOnboarding && !isOnboarding) return '/onboarding'; if (!needsOnboarding && isOnboarding) return '/home'; } return null; }, routes: [ // Splash shown while auth status is unknown. GoRoute( path: '/loading', builder: (_, __) => const _SplashScreen(), ), GoRoute( path: '/auth/login', builder: (_, __) => const LoginScreen(), ), GoRoute( path: '/auth/register', builder: (_, __) => const RegisterScreen(), ), // Onboarding — full-screen, no bottom nav, shown to new users. GoRoute( path: '/onboarding', builder: (_, __) => const OnboardingScreen(), ), // Full-screen recipe detail — shown without the bottom navigation bar. GoRoute( path: '/recipe-detail', builder: (context, state) { final extra = state.extra; if (extra is Recipe) { return RecipeDetailScreen(recipe: extra); } if (extra is SavedRecipe) { return RecipeDetailScreen(saved: extra); } return const _InvalidRoute(); }, ), // Add product — shown without the bottom navigation bar. GoRoute( path: '/products/add', builder: (_, __) => const AddProductScreen(), ), // Shopping list — full-screen, no bottom nav. GoRoute( path: '/menu/shopping-list', builder: (context, state) { final week = state.extra as String? ?? ''; return ShoppingListScreen(week: week); }, ), // Scan / recognition flow — all without bottom nav. GoRoute( path: '/scan', builder: (_, __) => const ScanScreen(), ), GoRoute( path: '/scan/confirm', builder: (context, state) { final items = state.extra as List? ?? []; return RecognitionConfirmScreen(items: items); }, ), GoRoute( path: '/scan/history', builder: (_, __) => const RecognitionHistoryScreen(), ), ShellRoute( builder: (context, state, child) => MainShell(child: child), routes: [ GoRoute(path: '/home', builder: (_, __) => const HomeScreen()), GoRoute( path: '/products', builder: (_, __) => const ProductsScreen()), GoRoute(path: '/menu', builder: (_, __) => const MenuScreen()), GoRoute( path: '/recipes', builder: (_, __) => const RecipesScreen()), GoRoute( path: '/profile', builder: (_, __) => const ProfileScreen()), ], ), ], ); }); class _SplashScreen extends StatelessWidget { const _SplashScreen(); @override Widget build(BuildContext context) => const Scaffold( body: Center(child: CircularProgressIndicator()), ); } class _InvalidRoute extends StatelessWidget { const _InvalidRoute(); @override Widget build(BuildContext context) { WidgetsBinding.instance.addPostFrameCallback((_) { if (context.mounted) Navigator.of(context).pop(); }); return const Scaffold(body: SizedBox.shrink()); } } class MainShell extends ConsumerWidget { final Widget child; const MainShell({super.key, required this.child}); static const _tabs = [ '/home', '/products', '/menu', '/recipes', '/profile', ]; @override Widget build(BuildContext context, WidgetRef ref) { final location = GoRouterState.of(context).matchedLocation; final currentIndex = _tabs.indexOf(location).clamp(0, _tabs.length - 1); // Count products expiring soon for the badge. final expiringCount = ref.watch(productsProvider).maybeWhen( data: (products) => products.where((p) => p.expiringSoon).length, orElse: () => 0, ); return Scaffold( body: child, bottomNavigationBar: BottomNavigationBar( currentIndex: currentIndex, onTap: (index) => context.go(_tabs[index]), items: [ const BottomNavigationBarItem( icon: Icon(Icons.home), label: 'Главная', ), BottomNavigationBarItem( icon: Badge( isLabelVisible: expiringCount > 0, label: Text('$expiringCount'), child: const Icon(Icons.kitchen), ), label: 'Продукты', ), const BottomNavigationBarItem( icon: Icon(Icons.calendar_month), label: 'Меню', ), const BottomNavigationBarItem( icon: Icon(Icons.menu_book), label: 'Рецепты', ), const BottomNavigationBarItem( icon: Icon(Icons.person), label: 'Профиль', ), ], ), ); } }