diff --git a/lib/router.dart b/lib/router.dart index 81eaf11..a30cfb4 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,6 +1,8 @@ +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'; @@ -13,20 +15,21 @@ import 'features/tags/tag_detail_screen.dart'; import 'features/settings/settings_screen.dart'; import 'providers.dart'; -const _unauthenticated = {'/connect', '/login', '/onboarding'}; +// Routes that manage their own auth state — no redirect applied. +const _unauthenticated = {'/', '/connect', '/login', '/onboarding'}; final routerProvider = Provider((ref) { return GoRouter( - initialLocation: '/connect', + 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; - final loc = state.matchedLocation; - if (!hasConfig && loc != '/connect') return '/connect'; - if (hasConfig && !hasToken && !_unauthenticated.contains(loc)) { - return '/login'; - } + if (!hasConfig) return '/connect'; + if (!hasToken) return '/login'; return null; }, routes: [ @@ -46,8 +49,10 @@ final routerProvider = Provider((ref) { path: '/onboarding', builder: (context, state) => const OnboardingScreen(), ), - StatefulShellRoute.indexedStack( + StatefulShellRoute( builder: (context, state, shell) => MainShell(shell: shell), + navigatorContainerBuilder: (context, shell, children) => + _SlidingTabSwitcher(index: shell.currentIndex, children: children), branches: [ StatefulShellBranch(routes: [ GoRoute( @@ -94,3 +99,87 @@ final routerProvider = Provider((ref) { ], ); }); + +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]), + ); + }), + ); + }, + ); + } +}