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/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; 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'; if (!loggedIn && !loggingIn) 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()), ], ); } @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.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: 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(onPressed: auth.logout, icon: const Icon(Icons.logout)), ], ), bottomNavigationBar: NavigationBar( selectedIndex: pageIndex, onDestinationSelected: (int index) => _onItemTapped(index, pageIndex), destinations: const [ NavigationDestination(icon: Icon(Icons.home), label: "Home"), NavigationDestination(icon: Icon(Icons.route), label: "Calculator"), NavigationDestination(icon: Icon(Icons.list), label: "Entries"), NavigationDestination(icon: Icon(Icons.train), label: "Traction"), NavigationDestination(icon: Icon(Icons.book), label: "Trips"), NavigationDestination(icon: Icon(Icons.add), label: "Add"), ], ), body: 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; } }