177 lines
5.3 KiB
Dart
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]),
|
|
);
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|