import 'package:app/core/i18n/translations.g.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; class AppShell extends StatefulWidget { final Widget child; const AppShell({super.key, required this.child}); @override State createState() => _AppShellState(); } class _AppShellState extends State { static const double _railBreakpoint = 800; // tablet and up bool _railExtended = true; // start "open" on tablet/desktop final FocusNode _contentFocus = FocusNode(debugLabel: 'contentFocus'); @override void dispose() { _contentFocus.dispose(); super.dispose(); } // --- NAV ITEMS ------------------------------------------------------------- List<({IconData icon, IconData? selectedIcon, String label, String route})> _getItems(BuildContext context) { final t = Translations.of(context); return [ ( icon: Icons.dashboard_outlined, selectedIcon: Icons.dashboard, label: t.app.navigationDashboard, route: '/home', ), ( icon: Icons.account_balance_wallet_outlined, selectedIcon: Icons.account_balance_wallet, label: t.app.navigationBudgets, route: '/budget', ), ( icon: Icons.inventory_2_outlined, selectedIcon: Icons.inventory_2, label: t.app.navigationInventory, route: '/inventory', ), ( icon: Icons.bar_chart_outlined, selectedIcon: Icons.bar_chart, label: t.app.navigationReports, route: '/reports', ), ( icon: Icons.settings, selectedIcon: Icons.settings_outlined, label: t.app.navigationSettings, route: '/settings', ), ]; } int _indexForPath(String p, List<({IconData icon, IconData? selectedIcon, String label, String route})> items) { for (var i = 0; i < items.length; i++) { if (p.startsWith(items[i].route)) return i; } return 0; } void _goForIndex(BuildContext ctx, int i, List<({IconData icon, IconData? selectedIcon, String label, String route})> items) => ctx.go(items[i].route); @override Widget build(BuildContext context) { final t = Translations.of(context); final items = _getItems(context); final width = MediaQuery.of(context).size.width; final isRail = width >= _railBreakpoint; final currentPath = GoRouterState.of(context).matchedLocation; // keep focus on right/content pane on wide layouts if (isRail) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && !_contentFocus.hasFocus) _contentFocus.requestFocus(); }); } final appBar = AppBar( title: _LogoHeader(), leading: isRail ? null : Builder( builder: (ctx) => IconButton( icon: const Icon(Icons.menu), onPressed: () => Scaffold.of(ctx).openDrawer(), tooltip: t.app.tooltipMenu, ), ), actions: [ IconButton( tooltip: t.app.tooltipNotifications, onPressed: () {}, icon: const Icon(Icons.notifications_none), ), IconButton( tooltip: t.app.tooltipUserSettings, onPressed: () => context.push('/settings'), icon: const Icon(Icons.account_circle_outlined), ), ], ); if (!isRail) { // ------------------- MOBILE: Drawer ------------------- final selectedIndex = _indexForPath(currentPath, items); return Scaffold( appBar: appBar, drawer: _AppDrawer(items: items, selectedIndex: selectedIndex), body: SafeArea(child: widget.child), ); } // ------------------- TABLET/DESKTOP: NavigationRail ------------------- final selected = _indexForPath(currentPath, items); final scheme = Theme.of(context).colorScheme; return Scaffold( appBar: appBar, body: Row( children: [ NavigationRailTheme( data: NavigationRailThemeData( groupAlignment: -1.0, // align to top useIndicator: true, indicatorColor: scheme.secondaryContainer, // background for selected "button" indicatorShape: const StadiumBorder(), selectedIconTheme: IconThemeData( color: scheme.onSecondaryContainer, ), selectedLabelTextStyle: TextStyle( color: scheme.onSecondaryContainer, fontWeight: FontWeight.w600, ), ), child: NavigationRail( extended: _railExtended, selectedIndex: selected, onDestinationSelected: (i) => _goForIndex(context, i, items), // leading: const Padding( // padding: EdgeInsets.only(top: 8), // child: _LogoHeader(), // ), destinations: [ for (final it in items) NavigationRailDestination( icon: Icon(it.icon), selectedIcon: Icon(it.selectedIcon ?? it.icon), label: Text(it.label), ), ], trailingAtBottom: true, trailing: Column( children: [ const SizedBox(height: 8), const Divider(height: 1), // toggle lives at the very bottom so layout doesn't jump IconButton( tooltip: _railExtended ? t.app.tooltipCollapseRail : t.app.tooltipExpandRail, onPressed: () => setState(() => _railExtended = !_railExtended), icon: Icon( _railExtended ? Icons.keyboard_double_arrow_left : Icons.keyboard_double_arrow_right, ), ), ], ), ), ), const VerticalDivider(width: 1), Expanded( child: SafeArea( child: Focus( focusNode: _contentFocus, autofocus: true, child: widget.child, ), ), ), ], ), ); } } class _AppDrawer extends StatelessWidget { final List< ({IconData icon, IconData? selectedIcon, String label, String route}) > items; final int selectedIndex; const _AppDrawer({required this.items, required this.selectedIndex}); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return Drawer( child: SafeArea( child: ListTileTheme( selectedColor: scheme.onSecondaryContainer, selectedTileColor: scheme.secondaryContainer, child: ListView( padding: const EdgeInsets.symmetric(vertical: 8), children: [ const _LogoHeader(), const Divider(), for (var i = 0; i < items.length; i++) ListTile( selected: i == selectedIndex, leading: Icon( i == selectedIndex ? (items[i].selectedIcon ?? items[i].icon) : items[i].icon, ), title: Text(items[i].label), onTap: () { Navigator.of(context).maybePop(); context.go(items[i].route); }, ), const Divider(), ListTile( leading: const Icon(Icons.settings_outlined), title: Text(Translations.of(context).app.drawerSettings), onTap: () { Navigator.of(context).maybePop(); context.go('/settings'); }, ), ], ), ), ), ); } } class _LogoHeader extends StatelessWidget { const _LogoHeader(); @override Widget build(BuildContext context) { return const Padding( padding: EdgeInsets.all(16), child: Row( children: [ Icon(Icons.account_balance_wallet, size: 28), SizedBox(width: 8), ], ), ); } }