import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../features/auth/presentation/controllers/dev_auth_bypass.dart'; /// Adaptive application shell. /// /// * Desktop / tablet (>= 1024 dp): a seven-destination [NavigationRail] /// (Medication, Nutrition, Exercise, Meal Plan, Recipes, Pantry, /// Settings) with `labelType` set to `all`; the rail is wrapped in a /// scrollable constrained to the available height so compact vertical /// windows never overflow. /// * Mobile: the four core daily actions live in the bottom /// [NavigationBar]; secondary destinations (Exercise, Recipes, Settings) /// are routed through an adaptive drawer opened from the "More" tab. class AdaptiveHomeScaffold extends ConsumerWidget { final Widget child; const AdaptiveHomeScaffold({ super.key, required this.child, }); /// Key for the mobile scaffold so the "More" bottom-bar tab can open the /// secondary-destinations drawer (the build context cannot reach a /// Scaffold constructed in the same method via [Scaffold.of]). /// /// Static: the shell is a singleton route scaffold. static final GlobalKey _mobileScaffoldKey = GlobalKey(); /// Desktop rail destinations in navigation order. static const _railDestinations = [ NavigationRailDestination( icon: Icon(Icons.medication_liquid_outlined), selectedIcon: Icon(Icons.medication_liquid), label: Text('Medication'), ), NavigationRailDestination( icon: Icon(Icons.restaurant_outlined), selectedIcon: Icon(Icons.restaurant), label: Text('Nutrition'), ), NavigationRailDestination( icon: Icon(Icons.fitness_center_outlined), selectedIcon: Icon(Icons.fitness_center), label: Text('Exercise'), ), NavigationRailDestination( icon: Icon(Icons.calendar_month_outlined), selectedIcon: Icon(Icons.calendar_month), label: Text('Meal Plan'), ), NavigationRailDestination( icon: Icon(Icons.menu_book_outlined), selectedIcon: Icon(Icons.menu_book), label: Text('Recipes'), ), NavigationRailDestination( icon: Icon(Icons.inventory_2_outlined), selectedIcon: Icon(Icons.inventory_2), label: Text('Pantry'), ), NavigationRailDestination( icon: Icon(Icons.settings_outlined), selectedIcon: Icon(Icons.settings), label: Text('Settings'), ), ]; /// Maps a location to its desktop rail index (0..6); secondary routes /// that share a rail slot resolve to that slot. int _railIndexFor(String location) { if (location.startsWith('/medication')) return 0; if (location.startsWith('/nutrition')) return 1; if (location.startsWith('/exercise')) return 2; if (location.startsWith('/meal-planner')) return 3; if (location.startsWith('/recipes')) return 4; if (location.startsWith('/pantry')) return 5; if (location.startsWith('/settings')) return 6; return 0; } /// Maps a location to its bottom-bar index; secondary destinations /// (exercise / recipes / settings) highlight the "More" tab. int _bottomIndexFor(String location) { if (location.startsWith('/medication')) return 0; if (location.startsWith('/nutrition')) return 1; if (location.startsWith('/meal-planner')) return 2; if (location.startsWith('/pantry')) return 3; return 4; // exercise, recipes, settings -> More } void _onRailSelected(int index, BuildContext context) { switch (index) { case 0: context.go('/medication'); break; case 1: context.go('/nutrition'); break; case 2: context.go('/exercise'); break; case 3: context.go('/meal-planner'); break; case 4: context.go('/recipes'); break; case 5: context.go('/pantry'); break; case 6: context.go('/settings'); break; } } @override Widget build(BuildContext context, WidgetRef ref) { final width = MediaQuery.of(context).size.width; final isDesktop = width >= 1024; final location = GoRouterState.of(context).uri.toString(); // Visual marker while the debug auth bypass is active, so a review // session can never be mistaken for a signed-in one. final showDevBanner = kDebugMode && ref.watch(devAuthBypassProvider); if (isDesktop) { return Scaffold( body: Column( children: [ if (showDevBanner) const _DevAccessBanner(), Expanded( child: Row( children: [ // Wrap the rail in a height-constrained scroll view so a // compact vertical window (or browser zoom) scrolls the // destinations instead of overflowing the render flex. LayoutBuilder( builder: (context, constraints) => SingleChildScrollView( child: ConstrainedBox( constraints: BoxConstraints(minHeight: constraints.maxHeight), child: IntrinsicHeight( child: NavigationRail( selectedIndex: _railIndexFor(location), onDestinationSelected: (idx) => _onRailSelected(idx, context), labelType: NavigationRailLabelType.all, destinations: _railDestinations, ), ), ), ), ), const VerticalDivider(thickness: 1, width: 1), Expanded(child: child), ], ), ), ], ), ); } final selectedIndex = _bottomIndexFor(location); return Scaffold( key: _mobileScaffoldKey, body: Column( children: [ if (showDevBanner) const _DevAccessBanner(), Expanded(child: child), ], ), drawer: const _SecondaryDestinationsDrawer(), bottomNavigationBar: NavigationBar( selectedIndex: selectedIndex, onDestinationSelected: (index) { if (index == 4) { // "More": open the drawer with secondary destinations. _mobileScaffoldKey.currentState?.openDrawer(); return; } switch (index) { case 0: context.go('/medication'); break; case 1: context.go('/nutrition'); break; case 2: context.go('/meal-planner'); break; case 3: context.go('/pantry'); break; } }, destinations: const [ NavigationDestination( icon: Icon(Icons.medication_liquid_outlined), selectedIcon: Icon(Icons.medication_liquid), label: 'Meds', ), NavigationDestination( icon: Icon(Icons.restaurant_outlined), selectedIcon: Icon(Icons.restaurant), label: 'Food', ), NavigationDestination( icon: Icon(Icons.calendar_month_outlined), selectedIcon: Icon(Icons.calendar_month), label: 'Plan', ), NavigationDestination( icon: Icon(Icons.inventory_2_outlined), selectedIcon: Icon(Icons.inventory_2), label: 'Pantry', ), NavigationDestination( icon: Icon(Icons.more_horiz), selectedIcon: Icon(Icons.more_horiz), label: 'More', ), ], ), ); } } /// Mobile drawer holding the secondary destinations that do not fit the /// four-slot bottom bar (Exercise, Recipes, Settings). class _SecondaryDestinationsDrawer extends StatelessWidget { const _SecondaryDestinationsDrawer(); @override Widget build(BuildContext context) { final location = GoRouterState.of(context).uri.toString(); return Drawer( child: SafeArea( child: ListView( padding: EdgeInsets.zero, children: [ Padding( padding: const EdgeInsets.all(16), child: Text( 'More', style: Theme.of(context) .textTheme .titleMedium ?.copyWith(fontWeight: FontWeight.bold), ), ), const Divider(height: 1), ListTile( leading: const Icon(Icons.fitness_center_outlined), title: const Text('Exercise'), selected: location.startsWith('/exercise'), onTap: () { Navigator.of(context).pop(); context.go('/exercise'); }, ), ListTile( leading: const Icon(Icons.menu_book_outlined), title: const Text('Recipes'), selected: location.startsWith('/recipes'), onTap: () { Navigator.of(context).pop(); context.go('/recipes'); }, ), ListTile( leading: const Icon(Icons.settings_outlined), title: const Text('Settings'), selected: location.startsWith('/settings'), onTap: () { Navigator.of(context).pop(); context.go('/settings'); }, ), ], ), ), ); } } /// Slim status strip shown while the development auth bypass is active. class _DevAccessBanner extends StatelessWidget { const _DevAccessBanner(); @override Widget build(BuildContext context) { return Material( color: Colors.orange, child: SafeArea( bottom: false, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: Row( children: [ const Icon(Icons.bug_report_outlined, size: 16, color: Colors.white), const SizedBox(width: 8), Expanded( child: Text( 'Development mode - not signed in', style: Theme.of(context).textTheme.labelMedium?.copyWith( color: Colors.white, ), ), ), ], ), ), ), ); } }