import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/api/common_providers.dart'; import '../../core/auth/auth_controller.dart'; import '../../core/widgets/service_mark.dart'; import '../accounts/account_create_dialog.dart'; import '../home/providers.dart' show dataQualityProvider; import 'nav_destinations.dart'; /// The widest the page content (and the content of the top bar) grows: on a big monitor a table /// stretched edge to edge is harder to read than one that stops. const kContentMaxWidth = 1440.0; /// The top bar of the wide layout (>=600): the primary destinations as pills, the ledger /// screens behind «Операции», and on the right «Добавить», the data-health counter, settings /// and the profile. Below 600 the shell uses a bottom bar instead. class TopNav extends ConsumerWidget { const TopNav({ required this.selectedIndex, required this.onSelect, super.key, }); final int selectedIndex; final ValueChanged onSelect; int _indexOf(String path) => navDestinations.indexWhere((d) => d.path == path); @override Widget build(BuildContext context, WidgetRef ref) { final scheme = Theme.of(context).colorScheme; final compact = MediaQuery.sizeOf(context).width < 900; final issueCount = ref.watch(dataQualityProvider).valueOrNull?.data.length ?? 0; final primary = [ for (var i = 0; i < navDestinations.length; i++) if (navDestinations[i].primary) i, ]; final ledger = [ for (var i = 0; i < navDestinations.length; i++) if (navDestinations[i].group == NavGroup.ledger) i, ]; final health = _indexOf('/health'); final settings = _indexOf('/settings'); return Container( height: 64, decoration: BoxDecoration( color: scheme.surfaceContainerLowest, border: Border(bottom: BorderSide(color: scheme.outlineVariant)), ), // the band runs edge to edge, but what is on it lines up with the page content below: // same maximum width, same 16 px side padding as the cards child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: kContentMaxWidth), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ Tooltip( message: 'Обзор', child: InkWell( customBorder: const CircleBorder(), onTap: () => onSelect(_indexOf('/')), child: const ServiceMark(), ), ), SizedBox(width: compact ? 12 : 28), Expanded( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ for (final i in primary) _Pill( label: navDestinations[i].label, selected: i == selectedIndex, onTap: () => onSelect(i), ), _LedgerMenu( indices: ledger, selectedIndex: selectedIndex, onSelect: onSelect, ), ], ), ), ), _AddMenu(compact: compact), const SizedBox(width: 4), IconButton( tooltip: 'Здоровье данных', onPressed: () => onSelect(health), icon: Badge( isLabelVisible: issueCount > 0, label: Text('$issueCount'), child: Icon( Icons.monitor_heart_outlined, color: selectedIndex == health ? scheme.primary : scheme.onSurfaceVariant, ), ), ), IconButton( tooltip: 'Настройки', onPressed: () => onSelect(settings), icon: Icon( Icons.settings_outlined, color: selectedIndex == settings ? scheme.primary : scheme.onSurfaceVariant, ), ), const _ProfileMenu(), ], ), ), ), ), ); } } class _Pill extends StatelessWidget { const _Pill({ required this.label, required this.selected, required this.onTap, }); final String label; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.only(right: 4), child: Material( color: selected ? scheme.surfaceContainerHighest : Colors.transparent, borderRadius: BorderRadius.circular(8), child: InkWell( borderRadius: BorderRadius.circular(8), onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), child: Text( label, style: TextStyle( color: selected ? scheme.onSurface : scheme.onSurfaceVariant, fontWeight: selected ? FontWeight.w600 : FontWeight.w500, ), ), ), ), ), ); } } /// «Операции»: the ledger screens, which are visited less often than the four main tabs. class _LedgerMenu extends StatelessWidget { const _LedgerMenu({ required this.indices, required this.selectedIndex, required this.onSelect, }); final List indices; final int selectedIndex; final ValueChanged onSelect; @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final active = indices.contains(selectedIndex); return PopupMenuButton( tooltip: 'Операции', position: PopupMenuPosition.under, onSelected: onSelect, itemBuilder: (_) => [ for (final i in indices) PopupMenuItem( value: i, child: Row( children: [ Icon( navDestinations[i].icon, size: 20, color: scheme.onSurfaceVariant, ), const SizedBox(width: 12), Text(navDestinations[i].label), ], ), ), ], child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( color: active ? scheme.surfaceContainerHighest : Colors.transparent, borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( active ? navDestinations[selectedIndex].label : 'Операции', style: TextStyle( color: active ? scheme.onSurface : scheme.onSurfaceVariant, fontWeight: active ? FontWeight.w600 : FontWeight.w500, ), ), Icon( Icons.arrow_drop_down, size: 20, color: scheme.onSurfaceVariant, ), ], ), ), ); } } /// «Добавить»: the four things a person creates by hand. class _AddMenu extends StatelessWidget { const _AddMenu({required this.compact}); final bool compact; @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return PopupMenuButton( tooltip: 'Добавить', position: PopupMenuPosition.under, onSelected: (v) => switch (v) { 'event' => context.go('/events'), 'account' => showAccountCreateDialog(context), 'portfolio' => context.go('/portfolios'), _ => context.go('/imports'), }, itemBuilder: (_) => const [ PopupMenuItem( value: 'event', child: ListTile( dense: true, leading: Icon(Icons.swap_vert), title: Text('Событие'), subtitle: Text('Сделка, пополнение, выплата'), ), ), PopupMenuItem( value: 'account', child: ListTile( dense: true, leading: Icon(Icons.account_balance_outlined), title: Text('Брокерский счёт'), ), ), PopupMenuItem( value: 'portfolio', child: ListTile( dense: true, leading: Icon(Icons.pie_chart_outline), title: Text('Портфель'), ), ), PopupMenuItem( value: 'import', child: ListTile( dense: true, leading: Icon(Icons.upload_file_outlined), title: Text('Отчёт брокера'), ), ), ], child: Container( padding: EdgeInsets.symmetric( horizontal: compact ? 10 : 16, vertical: 10, ), decoration: BoxDecoration( color: scheme.primary.withValues(alpha: 0.16), borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.add_circle_outline, size: 20, color: scheme.primary), if (!compact) ...[ const SizedBox(width: 8), Text( 'Добавить', style: TextStyle( color: scheme.primary, fontWeight: FontWeight.w600, ), ), ], ], ), ), ); } } class _ProfileMenu extends ConsumerWidget { const _ProfileMenu(); @override Widget build(BuildContext context, WidgetRef ref) { final scheme = Theme.of(context).colorScheme; final email = ref.watch(meProvider).valueOrNull?.email; final initial = (email?.isNotEmpty ?? false) ? email![0].toUpperCase() : '?'; return PopupMenuButton( tooltip: email ?? 'Профиль', position: PopupMenuPosition.under, onSelected: (_) => ref.read(authControllerProvider.notifier).logout(), itemBuilder: (_) => [ PopupMenuItem(enabled: false, child: Text(email ?? '…')), const PopupMenuItem( value: 'logout', child: ListTile( dense: true, leading: Icon(Icons.logout), title: Text('Выйти'), ), ), ], child: Padding( padding: const EdgeInsets.only(left: 8), child: CircleAvatar( radius: 18, backgroundColor: scheme.primaryContainer, child: Text( initial, style: TextStyle( color: scheme.onPrimaryContainer, fontWeight: FontWeight.w700, ), ), ), ), ); } }