/// Bottom navigation: products, lists, add, and shop log. library; import 'package:flutter/material.dart'; import 'products_screen.dart'; import 'scan_screen.dart'; import 'services/app_settings.dart'; import 'services/product_repository.dart'; import 'shop_log_screen.dart'; import 'shopping_lists_screen.dart'; class HomeShell extends StatefulWidget { const HomeShell({super.key, required this.repository}); final ProductRepository repository; @override State createState() => _HomeShellState(); } class _HomeShellState extends State { static const _addIndex = 2; static const _shopLogIndex = 3; static const _fabGap = 56.0; int _index = _addIndex; int _revision = 0; AppSettings? _settings; int _seenGeneration = 0; @override void didChangeDependencies() { super.didChangeDependencies(); final settings = SettingsScope.maybeOf(context); if (settings != _settings) { _settings?.removeListener(_onSettings); _settings = settings; _seenGeneration = settings?.catalogGeneration ?? 0; _settings?.addListener(_onSettings); } } @override void dispose() { _settings?.removeListener(_onSettings); super.dispose(); } void _onSettings() { final generation = _settings?.catalogGeneration ?? 0; if (generation == _seenGeneration) return; _seenGeneration = generation; if (mounted) setState(() => _revision++); } void _onTripSaved() { setState(() { _index = _shopLogIndex; _revision++; }); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; ScaffoldMessenger.of(context) .showSnackBar(const SnackBar(content: Text('Shopping trip saved.'))); }); } void _select(int index) { if (index == _index) return; setState(() => _index = index); } @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return Scaffold( body: switch (_index) { 0 => ProductsScreen( key: ValueKey('products-$_revision'), repository: widget.repository, ), 1 => ShoppingListsScreen( key: ValueKey('lists-$_revision'), repository: widget.repository, ), 2 => ScanScreen( repository: widget.repository, onTripSaved: _onTripSaved, ), _ => ShopLogScreen( key: ValueKey('shops-$_revision'), repository: widget.repository, ), }, floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, floatingActionButton: FloatingActionButton.small( onPressed: () => _select(_addIndex), tooltip: 'Add', elevation: _index == _addIndex ? 4 : 1, shape: const CircleBorder(), backgroundColor: scheme.primary, foregroundColor: scheme.onPrimary, child: const Icon(Icons.add, size: 24), ), bottomNavigationBar: BottomAppBar( shape: const CircularNotchedRectangle(), notchMargin: 6, elevation: 2, height: 52, padding: EdgeInsets.zero, color: scheme.surfaceContainer, child: Row( children: [ Expanded( child: Row( children: [ Expanded( child: _NavButton( icon: Icons.inventory_2_outlined, selectedIcon: Icons.inventory_2, label: 'Products', selected: _index == 0, onTap: () => _select(0), ), ), Expanded( child: _NavButton( icon: Icons.checklist_outlined, selectedIcon: Icons.checklist, label: 'Lists', selected: _index == 1, onTap: () => _select(1), ), ), ], ), ), const SizedBox(width: _fabGap), Expanded( child: _NavButton( icon: Icons.receipt_long_outlined, selectedIcon: Icons.receipt_long, label: 'Shop log', selected: _index == _shopLogIndex, onTap: () => _select(_shopLogIndex), ), ), ], ), ), ); } } class _NavButton extends StatelessWidget { const _NavButton({ required this.icon, required this.selectedIcon, required this.label, required this.selected, required this.onTap, }); final IconData icon; final IconData selectedIcon; final String label; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; final color = selected ? scheme.primary : scheme.onSurfaceVariant; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Icon(selected ? selectedIcon : icon, color: color, size: 20), Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.labelSmall?.copyWith( color: color, height: 1.1, fontSize: 10, fontWeight: selected ? FontWeight.w600 : FontWeight.w500, ), ), ], ), ); } }