import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; import 'package:go_router/go_router.dart'; import 'package:umbrix/gen/assets.gen.dart'; import 'package:umbrix/core/localization/translations.dart'; import 'package:umbrix/core/router/router.dart'; import 'package:umbrix/features/stats/widget/side_bar_stats_overview.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:umbrix/core/theme/theme_preferences.dart'; import 'package:umbrix/core/theme/app_theme_mode.dart'; import 'package:umbrix/core/localization/locale_preferences.dart'; import 'package:umbrix/core/localization/locale_extensions.dart'; import 'package:umbrix/utils/utils.dart'; abstract interface class RootScaffold { static final stateKey = GlobalKey(); static bool canShowDrawer(BuildContext context) => Breakpoints.small.isActive(context); } class AdaptiveRootScaffold extends HookConsumerWidget { const AdaptiveRootScaffold(this.navigator, {super.key}); final Widget navigator; @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); final interceptBackToHome = !PlatformUtils.isDesktop; final proxiesLocation = const ProxiesRoute().location; final perAppProxyLocation = const PerAppProxyRoute().location; final selectedIndex = getCurrentIndex(context); final destinations = [ NavigationDestination( icon: const Icon(FluentIcons.home_20_regular), selectedIcon: const Icon(FluentIcons.home_20_filled), label: t.home.pageTitle, ), NavigationDestination( icon: const Icon(FluentIcons.list_20_regular), selectedIcon: const Icon(FluentIcons.list_20_filled), label: t.proxies.pageTitle, ), NavigationDestination( icon: const Icon(FluentIcons.more_vertical_20_regular), selectedIcon: const Icon(FluentIcons.more_vertical_20_filled), label: t.settings.network.excludedDomains.pageTitle, ), NavigationDestination( icon: const Icon(FluentIcons.settings_20_filled), label: t.settings.pageTitle, ), NavigationDestination( icon: const Icon(FluentIcons.info_20_regular), selectedIcon: const Icon(FluentIcons.info_20_filled), label: t.about.pageTitle, ), ]; return _CustomAdaptiveScaffold( selectedIndex: selectedIndex, onSelectedIndexChange: (index) { RootScaffold.stateKey.currentState?.closeDrawer(); switchTab(index, context); }, destinations: destinations, drawerDestinationRange: (3, null), // Настройки и О программе всегда в drawer bottomDestinationRange: (0, 3), // Первые 3 пункта в bottom nav useBottomSheet: useMobileRouter, sidebarTrailing: const Expanded( child: Align( alignment: Alignment.bottomCenter, child: SideBarStatsOverview(), ), ), body: BackButtonListener( onBackButtonPressed: () async { if (!interceptBackToHome) return false; final location = GoRouterState.of(context).uri.path; final shouldGoHome = location.startsWith(proxiesLocation) || location.startsWith(perAppProxyLocation); assert(() { debugPrint( 'BACK_INTERCEPT AdaptiveRootScaffold location=$location shouldGoHome=$shouldGoHome', ); return true; }()); if (shouldGoHome) { const HomeRoute().go(context); return true; } return false; }, child: navigator, ), ); } } class _CustomAdaptiveScaffold extends HookConsumerWidget { const _CustomAdaptiveScaffold({ required this.selectedIndex, required this.onSelectedIndexChange, required this.destinations, required this.drawerDestinationRange, required this.bottomDestinationRange, this.useBottomSheet = false, this.sidebarTrailing, required this.body, }); final int selectedIndex; final Function(int) onSelectedIndexChange; final List destinations; final (int, int?) drawerDestinationRange; final (int, int?) bottomDestinationRange; final bool useBottomSheet; final Widget? sidebarTrailing; final Widget body; List destinationsSlice((int, int?) range) => destinations.sublist(range.$1, range.$2); int? selectedWithOffset((int, int?) range) { final index = selectedIndex - range.$1; return index < 0 || (range.$2 != null && index > (range.$2! - 1)) ? null : index; } void selectWithOffset(int index, (int, int?) range) => onSelectedIndexChange(index + range.$1); @override Widget build(BuildContext context, WidgetRef ref) { return Scaffold( key: RootScaffold.stateKey, drawer: Drawer( width: (MediaQuery.sizeOf(context).width * 0.88).clamp(1, 304), child: Column( children: [ // Логотип и название приложения Container( padding: const EdgeInsets.symmetric(vertical: 32), child: Column( children: [ Container( width: 80, height: 80, padding: const EdgeInsets.all(12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), color: Theme.of(context).colorScheme.primaryContainer, ), child: Assets.images.umbrixLogo.image( fit: BoxFit.contain, ), ), const SizedBox(height: 16), Text( 'Umbrix', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), ), ], ), ), // Список пунктов меню Expanded( child: ListView( padding: const EdgeInsets.symmetric(horizontal: 16), children: [ // О программе Builder( builder: (context) { final t = ref.watch(translationsProvider); return _DrawerMenuItem( icon: FluentIcons.info_24_regular, selectedIcon: FluentIcons.info_24_filled, label: t.about.pageTitle, isSelected: false, onTap: () { RootScaffold.stateKey.currentState?.closeDrawer(); const AboutRoute().push(context); }, ); }, ), // Настройки Builder( builder: (context) { final t = ref.watch(translationsProvider); return _DrawerMenuItem( icon: FluentIcons.settings_24_regular, selectedIcon: FluentIcons.settings_24_filled, label: t.settings.pageTitle, isSelected: false, onTap: () { RootScaffold.stateKey.currentState?.closeDrawer(); const SettingsRoute().push(context); }, ); }, ), const SizedBox(height: 16), const Divider(), const _DrawerThemeItem(), const _DrawerLanguageItem(), const _DrawerLicensesItem(), ], ), ), ], ), ), body: AdaptiveLayout( primaryNavigation: SlotLayout( config: { // Убираем боковую навигацию для Desktop }, ), body: SlotLayout( config: { Breakpoints.standard: SlotLayout.from( key: const Key('body'), inAnimation: AdaptiveScaffold.fadeIn, outAnimation: AdaptiveScaffold.fadeOut, builder: (context) => body, ), }, ), ), // Нижняя навигация - первые 3 пункта для всех платформ bottomNavigationBar: NavigationBar( selectedIndex: selectedWithOffset(bottomDestinationRange) ?? 0, destinations: destinationsSlice(bottomDestinationRange), onDestinationSelected: (index) => selectWithOffset(index, bottomDestinationRange), ), ); } } class _DrawerMenuItem extends StatelessWidget { const _DrawerMenuItem({ required this.icon, required this.selectedIcon, required this.label, required this.isSelected, required this.onTap, }); final IconData icon; final IconData selectedIcon; final String label; final bool isSelected; final VoidCallback onTap; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 4), child: ListTile( leading: Icon( isSelected ? selectedIcon : icon, size: 24, ), title: Text( label, style: TextStyle( fontSize: 16, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), ), selected: isSelected, selectedTileColor: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), onTap: onTap, contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), ), ); } } // Виджет для выбора темы в боковом меню class _DrawerThemeItem extends ConsumerWidget { const _DrawerThemeItem(); @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); final themeMode = ref.watch(themePreferencesProvider); return ListTile( leading: const Icon(FluentIcons.weather_moon_20_regular, size: 24), title: Text(t.settings.general.themeMode), subtitle: Text(themeMode.present(t)), onTap: () async { final selectedThemeMode = await showDialog( context: context, builder: (context) => SimpleDialog( title: Text(t.settings.general.themeMode), children: AppThemeMode.values .map((e) => RadioListTile( title: Text(e.present(t)), value: e, groupValue: themeMode, onChanged: Navigator.of(context).maybePop, )) .toList(), ), ); if (selectedThemeMode != null) { await ref.read(themePreferencesProvider.notifier).changeThemeMode(selectedThemeMode); } }, ); } } // Виджет для выбора языка в боковом меню class _DrawerLanguageItem extends ConsumerWidget { const _DrawerLanguageItem(); @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); final locale = ref.watch(localePreferencesProvider); return ListTile( leading: const Icon(FluentIcons.local_language_24_regular, size: 24), title: Text(t.settings.general.locale), subtitle: Text(locale.localeName), onTap: () async { final selectedLocale = await showDialog( context: context, builder: (context) => SimpleDialog( title: Text(t.settings.general.locale), children: AppLocale.values .map((e) => RadioListTile( title: Text(e.localeName), value: e, groupValue: locale, onChanged: Navigator.of(context).maybePop, )) .toList(), ), ); if (selectedLocale != null) { await ref.read(localePreferencesProvider.notifier).changeLocale(selectedLocale); } }, ); } } // Виджет для открытия лицензий class _DrawerLicensesItem extends ConsumerWidget { const _DrawerLicensesItem(); @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); return ListTile( leading: const Icon(FluentIcons.document_text_20_regular, size: 24), title: Text(MaterialLocalizations.of(context).licensesPageTitle), onTap: () { showLicensePage(context: context); }, ); } }