import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:mileograph_flutter/components/login/login.dart'; import 'package:mileograph_flutter/components/pages/calculator.dart'; import 'package:mileograph_flutter/components/pages/calculator_details.dart'; import 'package:mileograph_flutter/components/pages/dashboard.dart'; import 'package:mileograph_flutter/components/pages/legs.dart'; import 'package:mileograph_flutter/components/pages/loco_legs.dart'; import 'package:mileograph_flutter/components/pages/loco_timeline.dart'; import 'package:mileograph_flutter/components/pages/new_entry.dart'; import 'package:mileograph_flutter/components/pages/new_traction.dart'; import 'package:mileograph_flutter/components/pages/settings.dart'; import 'package:mileograph_flutter/components/pages/traction.dart'; import 'package:mileograph_flutter/components/pages/trips.dart'; import 'package:mileograph_flutter/services/authservice.dart'; import 'package:mileograph_flutter/services/data_service.dart'; import 'package:mileograph_flutter/services/navigation_guard.dart'; import 'package:provider/provider.dart'; final GlobalKey _shellNavigatorKey = GlobalKey(); const List _contentPages = [ "/", "/calculator", "/legs", "/traction", "/trips", "/add", ]; const int _addTabIndex = 5; class _NavItem { final String label; final IconData icon; const _NavItem(this.label, this.icon); } const List<_NavItem> _navItems = [ _NavItem("Home", Icons.home), _NavItem("Calculator", Icons.route), _NavItem("Entries", Icons.list), _NavItem("Traction", Icons.train), _NavItem("Trips", Icons.book), _NavItem("Add", Icons.add), ]; int tabIndexForPath(String path) { final newIndex = _contentPages.indexWhere((routePath) { if (path == routePath) return true; if (routePath == '/') return path == '/'; return path.startsWith('$routePath/'); }); return newIndex < 0 ? 0 : newIndex; } class MyApp extends StatefulWidget { const MyApp({super.key}); @override State createState() => _MyAppState(); } class _MyAppState extends State { late final GoRouter _router; bool _routerInitialized = false; final ColorScheme defaultLight = ColorScheme.fromSeed(seedColor: Colors.red); final ColorScheme defaultDark = ColorScheme.fromSeed( seedColor: Colors.red, brightness: Brightness.dark, ); @override void didChangeDependencies() { super.didChangeDependencies(); if (_routerInitialized) return; _routerInitialized = true; final auth = context.read(); _router = GoRouter( refreshListenable: auth, redirect: (context, state) { final loggedIn = auth.isLoggedIn; final loggingIn = state.uri.toString() == '/login'; final atSettings = state.uri.toString() == '/settings'; if (!loggedIn && !loggingIn && !atSettings) return '/login'; if (loggedIn && loggingIn) return '/'; return null; }, routes: [ ShellRoute( navigatorKey: _shellNavigatorKey, builder: (context, state, child) => MyHomePage(child: child), routes: [ GoRoute(path: '/', builder: (context, state) => const Dashboard()), GoRoute( path: '/calculator', builder: (context, state) => CalculatorPage(), ), GoRoute( path: '/calculator/details', builder: (context, state) => CalculatorDetailsPage(result: state.extra), ), GoRoute(path: '/legs', builder: (context, state) => LegsPage()), GoRoute( path: '/traction', builder: (context, state) => TractionPage(), ), GoRoute( path: '/traction/:id/timeline', builder: (_, state) { final idParam = state.pathParameters['id']; final locoId = int.tryParse(idParam ?? '') ?? 0; final extra = state.extra; String label = state.uri.queryParameters['label'] ?? ''; if (extra is Map && extra['label'] is String) { label = extra['label'] as String; } else if (extra is String && extra.isNotEmpty) { label = extra; } if (label.trim().isEmpty) label = 'Loco $locoId'; return LocoTimelinePage(locoId: locoId, locoLabel: label); }, ), GoRoute( path: '/traction/:id/legs', builder: (_, state) { final idParam = state.pathParameters['id']; final locoId = int.tryParse(idParam ?? '') ?? 0; final extra = state.extra; String label = state.uri.queryParameters['label'] ?? ''; if (extra is Map && extra['label'] is String) { label = extra['label'] as String; } else if (extra is String && extra.isNotEmpty) { label = extra; } if (label.trim().isEmpty) label = 'Loco $locoId'; return LocoLegsPage(locoId: locoId, locoLabel: label); }, ), GoRoute( path: '/traction/new', builder: (context, state) => const NewTractionPage(), ), GoRoute(path: '/trips', builder: (context, state) => TripsPage()), GoRoute(path: '/add', builder: (context, state) => NewEntryPage()), GoRoute( path: '/legs/edit/:id', builder: (_, state) { final idParam = state.pathParameters['id']; final legId = idParam == null ? null : int.tryParse(idParam); return NewEntryPage(editLegId: legId); }, ), ], ), GoRoute(path: '/login', builder: (context, state) => const LoginScreen()), GoRoute( path: '/settings', builder: (context, state) => const SettingsPage(), ), ], ); } @override Widget build(BuildContext context) { return DynamicColorBuilder( builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { return MaterialApp.router( title: 'Mileograph', routerConfig: _router, theme: ThemeData( useMaterial3: true, colorScheme: lightDynamic ?? defaultLight, ), darkTheme: ThemeData( useMaterial3: true, colorScheme: darkDynamic ?? defaultDark, ), themeMode: ThemeMode.system, ); }, ); } } class MyHomePage extends StatefulWidget { final Widget child; const MyHomePage({super.key, required this.child}); @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { List get contentPages => _contentPages; Future _onItemTapped(int index, int currentIndex) async { if (index < 0 || index >= contentPages.length || index == currentIndex) { return; } await NavigationGuard.attemptNavigation(() async { if (!mounted) return; context.go(contentPages[index]); }); } int? _lastTabIndex; final List _tabHistory = []; bool _handlingBackNavigation = false; bool _fetched = false; @override void didChangeDependencies() { super.didChangeDependencies(); if (_fetched) return; _fetched = true; WidgetsBinding.instance.addPostFrameCallback((_) { Future(() async { if (!mounted) return; final data = context.read(); final auth = context.read(); await auth.tryRestoreSession(); if (!auth.isLoggedIn) return; data.fetchEventFields(); if (data.homepageStats == null) { data.fetchHomepageStats(); } if (data.legs.isEmpty) { data.fetchLegs(); } if (data.traction.isEmpty) { data.fetchHadTraction(); } if (data.latestLocoChanges.isEmpty) { data.fetchLatestLocoChanges(); } if (data.onThisDay.isEmpty) { data.fetchOnThisDay(); } if (data.tripDetails.isEmpty) { data.fetchTripDetails(); } }); }); } @override Widget build(BuildContext context) { final uri = GoRouterState.of(context).uri; final pageIndex = tabIndexForPath(uri.path); _recordTabChange(pageIndex); if (pageIndex != _addTabIndex) { NavigationGuard.unregister(); } final homepageReady = context.select( (data) => data.homepageStats != null || !data.isHomepageLoading, ); final auth = context.read(); final currentPage = homepageReady ? widget.child : const Center(child: CircularProgressIndicator()); return PopScope( canPop: false, onPopInvokedWithResult: (didPop, _) async { if (didPop) return; final shellNav = _shellNavigatorKey.currentState; if (shellNav != null && shellNav.canPop()) { shellNav.pop(); return; } if (_tabHistory.isNotEmpty) { final previousTab = _tabHistory.removeLast(); if (!mounted) return; _handlingBackNavigation = true; context.go(contentPages[previousTab]); return; } if (pageIndex != 0) { if (!mounted) return; _handlingBackNavigation = true; context.go(contentPages[0]); return; } SystemNavigator.pop(); }, child: LayoutBuilder( builder: (context, constraints) { final isWide = constraints.maxWidth >= 900; final railExtended = constraints.maxWidth >= 1400; final navRailDestinations = _navItems .map( (item) => NavigationRailDestination( icon: Icon(item.icon), label: Text(item.label), ), ) .toList(); final navBarDestinations = _navItems .map( (item) => NavigationDestination( icon: Icon(item.icon), label: item.label, ), ) .toList(); return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text.rich( TextSpan( children: const [ TextSpan(text: "Mile"), TextSpan(text: "O", style: TextStyle(color: Colors.red)), TextSpan(text: "graph"), ], style: const TextStyle( decoration: TextDecoration.none, color: Colors.white, fontFamily: "Tomatoes", ), ), ), actions: [ const IconButton( onPressed: null, icon: Icon(Icons.account_circle), ), IconButton( tooltip: 'Settings', onPressed: () => context.go('/settings'), icon: const Icon(Icons.settings), ), IconButton(onPressed: auth.logout, icon: const Icon(Icons.logout)), ], ), bottomNavigationBar: isWide ? null : NavigationBar( selectedIndex: pageIndex, onDestinationSelected: (int index) => _onItemTapped(index, pageIndex), destinations: navBarDestinations, ), body: isWide ? Row( children: [ SafeArea( child: NavigationRail( selectedIndex: pageIndex, extended: railExtended, labelType: railExtended ? NavigationRailLabelType.none : NavigationRailLabelType.selected, onDestinationSelected: (int index) => _onItemTapped(index, pageIndex), destinations: navRailDestinations, ), ), const VerticalDivider(width: 1), Expanded(child: currentPage), ], ) : currentPage, ); }, ), ); } void _recordTabChange(int pageIndex) { final last = _lastTabIndex; if (last == null) { _lastTabIndex = pageIndex; return; } if (last == pageIndex) return; if (_handlingBackNavigation) { _handlingBackNavigation = false; _lastTabIndex = pageIndex; return; } if (_tabHistory.isEmpty || _tabHistory.last != last) { _tabHistory.add(last); } _lastTabIndex = pageIndex; } }