import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../features/diagnostics/presentation/diagnostics_page.dart'; import '../features/observability/presentation/observability_controller.dart'; import '../features/overview/presentation/overview_page.dart'; import '../shared/widgets/glass_panel.dart'; enum _Destination { overview, diagnostics } class AppShell extends ConsumerStatefulWidget { const AppShell({super.key}); @override ConsumerState createState() => _AppShellState(); } class _AppShellState extends ConsumerState { _Destination _destination = _Destination.overview; @override Widget build(BuildContext context) { final state = ref.watch(observabilityControllerProvider); const pages = [OverviewPage(), DiagnosticsPage()]; return Scaffold( body: OrkavianBackdrop( child: SafeArea( child: Stack( children: [ LayoutBuilder( builder: (context, constraints) { final compact = constraints.maxWidth < 760; final content = _Workspace( state: state, selectedIndex: _destination.index, pages: pages, compact: compact, ); return compact ? _CompactShell( selectedIndex: _destination.index, onSelected: _selectDestination, content: content, ) : _WideShell( selectedIndex: _destination.index, onSelected: _selectDestination, content: content, ); }, ), if (state.accessibilityAnnouncement case final announcement?) _CriticalAnnouncement(message: announcement), ], ), ), ), ); } void _selectDestination(int index) { setState(() => _destination = _Destination.values[index]); } } class _WideShell extends StatelessWidget { const _WideShell({ required this.selectedIndex, required this.onSelected, required this.content, }); final int selectedIndex; final ValueChanged onSelected; final Widget content; @override Widget build(BuildContext context) { return Row( children: [ GlassPanel( borderRadius: BorderRadius.zero, child: NavigationRail( selectedIndex: selectedIndex, labelType: NavigationRailLabelType.all, onDestinationSelected: onSelected, leading: const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Icon(Icons.flight_takeoff_outlined, size: 28), ), destinations: _destinations, ), ), const VerticalDivider(width: 1), Expanded(child: content), ], ); } } class _CompactShell extends StatelessWidget { const _CompactShell({ required this.selectedIndex, required this.onSelected, required this.content, }); final int selectedIndex; final ValueChanged onSelected; final Widget content; @override Widget build(BuildContext context) { return Column( children: [ Expanded(child: content), GlassPanel( borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), child: NavigationBar( selectedIndex: selectedIndex, onDestinationSelected: onSelected, destinations: const [ NavigationDestination( icon: Icon(Icons.space_dashboard_outlined), selectedIcon: Icon(Icons.space_dashboard), label: 'Overview', ), NavigationDestination( icon: Icon(Icons.receipt_long_outlined), selectedIcon: Icon(Icons.receipt_long), label: 'Diagnostics', ), ], ), ), ], ); } } const _destinations = [ NavigationRailDestination( icon: Icon(Icons.space_dashboard_outlined), selectedIcon: Icon(Icons.space_dashboard), label: Text('Overview'), ), NavigationRailDestination( icon: Icon(Icons.receipt_long_outlined), selectedIcon: Icon(Icons.receipt_long), label: Text('Diagnostics'), ), ]; class _Workspace extends StatelessWidget { const _Workspace({ required this.state, required this.selectedIndex, required this.pages, required this.compact, }); final ObservabilityState state; final int selectedIndex; final List pages; final bool compact; @override Widget build(BuildContext context) { return Column( children: [ _ConnectionBar(state: state, compact: compact), const Divider(height: 1), Expanded( child: IndexedStack(index: selectedIndex, children: pages), ), ], ); } } class _ConnectionBar extends StatelessWidget { const _ConnectionBar({required this.state, required this.compact}); final ObservabilityState state; final bool compact; @override Widget build(BuildContext context) { final theme = Theme.of(context); final heartbeat = state.lastHeartbeat; final peer = heartbeat == null ? 'No vehicle identity observed' : 'System ${heartbeat.systemId} / component ${heartbeat.componentId}'; final frameTime = state.lastFrameAt == null ? 'No frames received' : 'Last frame ${_formatTimestamp(state.lastFrameAt!)}'; return Semantics( label: '${state.status.label}; ${state.sourceLabel}; $peer; $frameTime', child: Padding( padding: EdgeInsets.symmetric( horizontal: compact ? 16 : 24, vertical: 12, ), child: GlassPanel( child: Padding( padding: const EdgeInsets.all(12), child: Wrap( spacing: 8, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ Icon( Icons.visibility_outlined, color: theme.colorScheme.primary, ), Text('Observation only', style: theme.textTheme.titleSmall), _StatusChip(label: state.sourceLabel), _StatusChip(label: state.status.label), Text(peer), Text(frameTime), ], ), ), ), ), ); } } class _CriticalAnnouncement extends StatelessWidget { const _CriticalAnnouncement({required this.message}); final String message; @override Widget build(BuildContext context) { return Semantics( container: true, liveRegion: true, label: message, child: const SizedBox.square(dimension: 1), ); } } class _StatusChip extends StatelessWidget { const _StatusChip({required this.label}); final String label; @override Widget build(BuildContext context) { return Chip(label: Text(label), visualDensity: VisualDensity.compact); } } String _formatTimestamp(DateTime value) => value.toIso8601String();