new startup screen logic

This commit is contained in:
Mikkeli Matlock
2026-02-08 02:56:32 +09:00
parent f2c69587ee
commit 9173c3b93a
2 changed files with 45 additions and 39 deletions

View File

@@ -19,7 +19,7 @@ class AppRoot extends StatefulWidget {
class _AppRootState extends State<AppRoot> { class _AppRootState extends State<AppRoot> {
bool _initialized = false; bool _initialized = false;
bool _overheatTriggered = false; bool _overheatTriggered = false;
String _initStatus = 'Starting...'; final Map<String, String> _initStatuses = {};
@override @override
void initState() { void initState() {
@@ -33,27 +33,32 @@ class _AppRootState extends State<AppRoot> {
super.dispose(); super.dispose();
} }
void _updateStatus(String key, String value) {
setState(() => _initStatuses[key] = value);
}
Future<void> _runInitSequence() async { Future<void> _runInitSequence() async {
// Load config first // Show all items from the start so the row doesn't jump around
setState(() => _initStatus = 'Loading config...'); _updateStatus('Config', '...');
_updateStatus('UART', '...');
_updateStatus('Navigator', '...');
// Config must load first (everything else depends on it)
_updateStatus('Config', 'Loading');
await ConfigService.instance.load(); await ConfigService.instance.load();
_updateStatus('Config', 'Ready');
setState(() => _initStatus = 'Checking systems...'); // UART health check and navigator image preload run truly in parallel
_updateStatus('UART', 'Connecting');
_updateStatus('Navigator', 'Loading');
await Future.wait([
_waitForUart(),
_preloadNavigatorImages(),
]);
// Let the user see the all-ready state for a moment
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 500));
// Check UART connection via backend health endpoint
// Also preload navigator images in parallel (usually UART is the bottleneck)
setState(() => _initStatus = 'UART: connecting...');
final imagePreloadFuture = _preloadNavigatorImages();
await _waitForUart();
await imagePreloadFuture;
setState(() => _initStatus = 'GPS: standby');
await Future.delayed(const Duration(milliseconds: 400));
setState(() => _initStatus = 'Ready');
await Future.delayed(const Duration(milliseconds: 300));
// Start overheat monitoring // Start overheat monitoring
OverheatMonitor.instance.start( OverheatMonitor.instance.start(
onOverheat: () { onOverheat: () {
@@ -78,11 +83,8 @@ class _AppRootState extends State<AppRoot> {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>; final data = jsonDecode(response.body) as Map<String, dynamic>;
final arduinoOk = data['arduino_connected'] == true; if (data['arduino_connected'] == true) {
_updateStatus('UART', 'Ready');
if (arduinoOk) {
setState(() => _initStatus = 'UART: OK');
await Future.delayed(const Duration(milliseconds: 300));
return; return;
} }
} }
@@ -90,28 +92,24 @@ class _AppRootState extends State<AppRoot> {
// Backend not reachable yet - keep trying // Backend not reachable yet - keep trying
} }
// Not connected yet _updateStatus('UART', 'Waiting');
setState(() => _initStatus = 'UART: waiting...');
await Future.delayed(retryDelay); await Future.delayed(retryDelay);
} }
// Timeout - proceed anyway (UI will show stale data indicators) // Timeout - proceed anyway (UI will show stale data indicators)
setState(() => _initStatus = 'UART: timeout'); _updateStatus('UART', 'Timeout');
await Future.delayed(const Duration(milliseconds: 500));
} }
/// Preload navigator images into Flutter's image cache /// Preload navigator images into Flutter's image cache
/// ///
/// Scans for all PNGs in the navigator folder and precaches them. /// Scans for all PNGs in the navigator folder and precaches them.
/// Runs silently - no status updates (meant to run parallel with UART).
Future<void> _preloadNavigatorImages() async { Future<void> _preloadNavigatorImages() async {
final images = await ConfigService.instance.getNavigatorImages(); final images = await ConfigService.instance.getNavigatorImages();
for (final file in images) { for (final file in images) {
// precacheImage needs a context, but we're in initState territory
// Use the root context via a post-frame callback workaround
if (!mounted) return; if (!mounted) return;
await precacheImage(FileImage(file), context); await precacheImage(FileImage(file), context);
} }
_updateStatus('Navigator', 'Ready');
} }
@override @override
@@ -121,7 +119,7 @@ class _AppRootState extends State<AppRoot> {
if (_overheatTriggered) { if (_overheatTriggered) {
child = const OverheatScreen(key: ValueKey('overheat')); child = const OverheatScreen(key: ValueKey('overheat'));
} else if (!_initialized) { } else if (!_initialized) {
child = SplashScreen(key: const ValueKey('splash'), status: _initStatus); child = SplashScreen(key: const ValueKey('splash'), statuses: _initStatuses);
} else { } else {
child = const DashboardScreen(key: ValueKey('dashboard')); child = const DashboardScreen(key: ValueKey('dashboard'));
} }

View File

@@ -3,10 +3,12 @@ import 'package:flutter/material.dart';
import '../theme/app_theme.dart'; import '../theme/app_theme.dart';
/// Splash screen - shown during initialization /// Splash screen - shown during initialization
///
/// Displays parallel status items that independently flip to "Ready".
class SplashScreen extends StatelessWidget { class SplashScreen extends StatelessWidget {
final String status; final Map<String, String> statuses;
const SplashScreen({super.key, required this.status}); const SplashScreen({super.key, required this.statuses});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -33,13 +35,19 @@ class SplashScreen extends StatelessWidget {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 32),
Text( Row(
status, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: statuses.entries.map((entry) {
final isReady = entry.value == 'Ready';
return Text(
'${entry.key}: ${entry.value}',
style: Theme.of(context).textTheme.bodyLarge?.copyWith( style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontSize: 80, fontSize: 48,
color: theme.subdued, color: isReady ? theme.foreground : theme.subdued,
), ),
);
}).toList(),
), ),
], ],
), ),