import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'features/connection/startup_screen.dart'; import 'features/connection/server_discovery_screen.dart'; import 'features/connection/login_screen.dart'; import 'features/onboarding/onboarding_screen.dart'; import 'features/shell/main_shell.dart'; import 'features/floorplan/floor_plan_screen.dart'; import 'features/sensors/sensor_list_screen.dart'; import 'features/tags/tag_list_screen.dart'; import 'features/settings/settings_screen.dart'; import 'providers.dart'; // Routes that manage their own auth state — no redirect applied. const _unauthenticated = {'/', '/connect', '/login', '/onboarding'}; final routerProvider = Provider((ref) { return GoRouter( initialLocation: '/', redirect: (context, state) { final loc = state.matchedLocation; if (_unauthenticated.contains(loc)) return null; final hasConfig = ref.read(serverConfigProvider) != null; final hasToken = ref.read(authTokenProvider) != null; if (!hasConfig) return '/connect'; if (!hasToken) return '/login'; return null; }, routes: [ GoRoute( path: '/', builder: (context, state) => const StartupScreen(), ), GoRoute( path: '/connect', builder: (context, state) => const ServerDiscoveryScreen(), ), GoRoute( path: '/login', builder: (context, state) => const LoginScreen(), ), GoRoute( path: '/onboarding', builder: (context, state) => const OnboardingScreen(), ), StatefulShellRoute( builder: (context, state, shell) => MainShell(shell: shell), navigatorContainerBuilder: (context, shell, children) => _SlidingTabSwitcher(index: shell.currentIndex, children: children), branches: [ StatefulShellBranch(routes: [ GoRoute( path: '/floorplan', builder: (context, state) => const FloorPlanScreen(), ), ]), StatefulShellBranch(routes: [ GoRoute( path: '/sensors', builder: (context, state) => const SensorListScreen(), ), ]), StatefulShellBranch(routes: [ GoRoute( path: '/tags', builder: (context, state) => const TagListScreen(), ), ]), StatefulShellBranch(routes: [ GoRoute( path: '/settings', builder: (context, state) => const SettingsScreen(), ), ]), ], ), ], ); }); class _SlidingTabSwitcher extends StatefulWidget { const _SlidingTabSwitcher({required this.index, required this.children}); final int index; final List children; @override State<_SlidingTabSwitcher> createState() => _SlidingTabSwitcherState(); } class _SlidingTabSwitcherState extends State<_SlidingTabSwitcher> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; late int _current; int _previous = -1; bool _forward = true; @override void initState() { super.initState(); _current = widget.index; _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 220), value: 1.0, ); _animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut); } @override void didUpdateWidget(_SlidingTabSwitcher old) { super.didUpdateWidget(old); if (widget.index != _current) { _previous = _current; _forward = widget.index > _current; setState(() => _current = widget.index); _controller.forward(from: 0); } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _animation, builder: (context, _) { return Stack( fit: StackFit.expand, children: List.generate(widget.children.length, (i) { if (i == _current) { final begin = Offset(_forward ? 1.0 : -1.0, 0); final offset = Offset.lerp(begin, Offset.zero, _animation.value)!; return FractionalTranslation( translation: offset, child: TickerMode(enabled: true, child: widget.children[i]), ); } if (i == _previous) { final end = Offset(_forward ? -1.0 : 1.0, 0); final offset = Offset.lerp(Offset.zero, end, _animation.value)!; return FractionalTranslation( translation: offset, child: IgnorePointer( child: TickerMode(enabled: false, child: widget.children[i]), ), ); } return Offstage( offstage: true, child: TickerMode(enabled: false, child: widget.children[i]), ); }), ); }, ); } }