Files
companion/lib/router.dart
T

177 lines
5.3 KiB
Dart

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/tags/tag_detail_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<GoRouter>((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(),
routes: [
GoRoute(
path: ':id',
builder: (context, state) => TagDetailScreen(
tagId: state.pathParameters['id']!,
),
),
],
),
]),
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<Widget> children;
@override
State<_SlidingTabSwitcher> createState() => _SlidingTabSwitcherState();
}
class _SlidingTabSwitcherState extends State<_SlidingTabSwitcher>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _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]),
);
}),
);
},
);
}
}