110 lines
3.6 KiB
Dart
110 lines
3.6 KiB
Dart
import "package:flutter/material.dart";
|
|
|
|
import "api.dart";
|
|
import "app_settings.dart";
|
|
import "auth.dart";
|
|
import "biometric.dart";
|
|
import "theme.dart";
|
|
import "screens/lock_screen.dart";
|
|
import "screens/login_screen.dart";
|
|
import "screens/root_shell.dart";
|
|
|
|
// Simple app-wide singletons (small app — no DI framework needed).
|
|
final apiClient = ApiClient();
|
|
final authService = AuthService(apiClient);
|
|
final appSettings = AppSettings();
|
|
|
|
Future<void> main() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
await apiClient.loadServerUrl();
|
|
await appSettings.loadFromStorage();
|
|
await authService.loadFromStorage();
|
|
// Prime the biometric-enabled cache; if a session survived from a previous
|
|
// run and biometric login is on, start locked so the app requires an unlock
|
|
// instead of jumping straight to the dashboard.
|
|
final bioEnabled = await biometricAuth.isEnabled();
|
|
if (authService.isAuthenticated && bioEnabled) {
|
|
authService.lock();
|
|
}
|
|
// If a token survived from a previous run, refresh the profile so the saved
|
|
// appearance prefs (theme/font/date format) apply on boot.
|
|
if (authService.isAuthenticated) {
|
|
apiClient.getMe().then((p) => appSettings.applyFromProfile(p)).catchError((_) {});
|
|
}
|
|
runApp(const CarControlApp());
|
|
}
|
|
|
|
class CarControlApp extends StatefulWidget {
|
|
const CarControlApp({super.key});
|
|
@override
|
|
State<CarControlApp> createState() => _CarControlAppState();
|
|
}
|
|
|
|
class _CarControlAppState extends State<CarControlApp> with WidgetsBindingObserver {
|
|
// Grace period: a quick app-switch (returning within this window) does NOT
|
|
// re-lock; staying in the background longer does.
|
|
static const _lockGrace = Duration(seconds: 30);
|
|
DateTime? _backgroundedAt;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
// Re-lock when the app returns from the background, but only if it was away
|
|
// longer than the grace period — so quick app-switches don't re-lock.
|
|
// Handle only 'paused'/'resumed' (not 'inactive', which also fires
|
|
// transiently while the OS biometric prompt is showing).
|
|
if (state == AppLifecycleState.paused) {
|
|
_backgroundedAt = DateTime.now();
|
|
} else if (state == AppLifecycleState.resumed) {
|
|
final since = _backgroundedAt;
|
|
_backgroundedAt = null;
|
|
if (since != null &&
|
|
DateTime.now().difference(since) > _lockGrace &&
|
|
authService.isAuthenticated &&
|
|
biometricAuth.enabledCached) {
|
|
authService.lock();
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListenableBuilder(
|
|
listenable: appSettings,
|
|
builder: (context, _) => MaterialApp(
|
|
title: "DriverVault",
|
|
debugShowCheckedModeBanner: false,
|
|
theme: DriverVault.theme(Brightness.light),
|
|
darkTheme: DriverVault.theme(Brightness.dark),
|
|
themeMode: appSettings.themeMode,
|
|
// Apply the user's font-size preference app-wide.
|
|
builder: (context, child) => MediaQuery(
|
|
data: MediaQuery.of(context).copyWith(
|
|
textScaler: TextScaler.linear(appSettings.textScale),
|
|
),
|
|
child: child!,
|
|
),
|
|
home: ListenableBuilder(
|
|
listenable: authService,
|
|
builder: (context, _) {
|
|
if (!authService.isAuthenticated) return const LoginScreen();
|
|
if (authService.locked) return const LockScreen();
|
|
return const RootShell();
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|