import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'analytics_tabs.dart'; import 'nav_destinations.dart'; import 'top_nav.dart'; /// Below this width the shell falls back to a bottom [NavigationBar]; from here up it is the /// [TopNav] bar. const _narrowBreakpoint = 600.0; /// Adaptive navigation shell around the current route: a bottom [NavigationBar] on narrow /// surfaces and the [TopNav] bar on wider ones, with the Аналитика tab strip under it while /// one of the analytics screens is open. /// /// The top bar shows every destination (the ledger ones behind «Операции»). The bottom bar /// shows the four primary ones plus «Ещё», which opens the rest in a sheet: twelve /// destinations in a phone-width bar would leave about 30 px per label, which is not /// navigation but decoration. class AppShell extends StatelessWidget { const AppShell({required this.location, required this.child, super.key}); final String location; final Widget child; int get _selectedIndex { // longest prefix wins, so /portfolio/instrument/311 keeps Портфель selected var best = -1; var bestLength = -1; for (var i = 0; i < navDestinations.length; i++) { final length = navDestinations[i].matchLength(location); if (length > bestLength) { best = i; bestLength = length; } } return best == -1 || bestLength < 0 ? 0 : best; } void _onSelect(BuildContext context, int index) { if (index != _selectedIndex) context.go(navDestinations[index].path); } @override Widget build(BuildContext context) { final width = MediaQuery.sizeOf(context).width; if (width < _narrowBreakpoint) { final primary = navDestinations.where((d) => d.primary).toList(); final selected = navDestinations[_selectedIndex]; final primaryIndex = primary.indexOf(selected); return Scaffold( body: SafeArea(child: child), bottomNavigationBar: NavigationBar( // nothing primary matches ⇒ we are inside a screen that lives behind «Ещё», // and «Ещё» is what should look active selectedIndex: primaryIndex >= 0 ? primaryIndex : primary.length, onDestinationSelected: (i) { if (i >= primary.length) { _showMore(context, selected); } else { final target = navDestinations.indexOf(primary[i]); _onSelect(context, target); } }, destinations: [ for (final d in primary) NavigationDestination( icon: Icon(d.icon), selectedIcon: Icon(d.selectedIcon), label: d.label, ), NavigationDestination( icon: const Icon(Icons.more_horiz), selectedIcon: const Icon(Icons.more_horiz), label: primaryIndex >= 0 ? 'Ещё' : selected.label, ), ], ), ); } return Scaffold( body: Column( children: [ TopNav( selectedIndex: _selectedIndex, onSelect: (i) => _onSelect(context, i), ), Expanded( child: Align( alignment: Alignment.topCenter, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: kContentMaxWidth), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (AnalyticsTabs.contains(location)) Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), child: AnalyticsTabs(location: location), ), Expanded(child: child), ], ), ), ), ), ], ), ); } /// The non-primary destinations, as a sheet. The currently open one is ticked, so «Ещё» /// still answers "where am I". void _showMore(BuildContext context, NavDestination current) { showModalBottomSheet( context: context, showDragHandle: true, builder: (sheetContext) => SafeArea( child: ListView( shrinkWrap: true, children: [ for (final d in navDestinations.where((d) => !d.primary)) ListTile( leading: Icon(d == current ? d.selectedIcon : d.icon), title: Text(d.label), selected: d == current, trailing: d == current ? const Icon(Icons.check) : null, onTap: () { Navigator.of(sheetContext).pop(); if (d != current) context.go(d.path); }, ), ], ), ), ); } }