import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../services/compass_service.dart'; import '../rides/ride_state_machine.dart'; import '../../models/telemetry_sample.dart'; import '../../services/telemetry_service.dart'; import 'dashboard_config.dart'; import 'widgets/g_force_visualizer.dart'; import 'widgets/lean_visualizer.dart'; import 'widgets/evolution_portrait_dashboard.dart'; import 'widgets/evolution_landscape_dashboard.dart'; import '../../core/theme/evolution_theme_config.dart'; import '../../services/alerts/road_alert_models.dart'; import '../../services/alerts/road_alert_service.dart'; import '../../services/location_service.dart'; class DashboardScreen extends ConsumerWidget { const DashboardScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final telemetryAsync = ref.watch(telemetryStreamProvider); final rideSession = ref.watch(rideStateMachineProvider); final layoutMode = ref.watch(dashboardConfigProvider); final themeMode = ref.watch(dashboardThemeModeProvider); final alertsAsync = ref.watch(activeAlertsStreamProvider); final hasLocPermissionAsync = ref.watch(locationPermissionGrantedProvider); final hasLocPermission = hasLocPermissionAsync.value ?? true; final activeAlerts = alertsAsync.value ?? []; final closestAlert = activeAlerts.isNotEmpty ? activeAlerts.first : null; TelemetrySample? telemetry = telemetryAsync.value; double currentSpeedMph = telemetry?.speed ?? 0.0; double heading = telemetry?.heading ?? 0.0; double leanAngle = telemetry?.leanAngle ?? 0.0; double accelG = telemetry?.longitudinalG ?? 0.0; double lateralG = telemetry?.lateralG ?? 0.0; return Theme( data: ThemeData.dark().copyWith( scaffoldBackgroundColor: Colors.black, colorScheme: const ColorScheme.dark( primary: Colors.cyanAccent, secondary: Colors.amberAccent, surface: Color(0xFF111111), ), ), child: Scaffold( body: SafeArea( child: Column( children: [ if (!hasLocPermission) _buildPermissionBanner(context, ref), if (closestAlert != null) _buildAlertBanner(closestAlert), Expanded( child: LayoutBuilder( builder: (context, constraints) { final isLandscape = constraints.maxWidth > constraints.maxHeight; if (themeMode == DashboardThemeMode.evolution) { if (isLandscape) { return EvolutionLandscapeDashboard( currentSpeedMph: currentSpeedMph, heading: heading, leanAngle: leanAngle, accelG: accelG, lateralG: lateralG, session: rideSession, telemetry: telemetry, ); } return EvolutionPortraitDashboard( currentSpeedMph: currentSpeedMph, heading: heading, leanAngle: leanAngle, accelG: accelG, lateralG: lateralG, session: rideSession, telemetry: telemetry, ); } if (layoutMode == DashboardLayout.minimal) { return _buildMinimalLayout(context, currentSpeedMph, session: rideSession, ref: ref); } if (layoutMode == DashboardLayout.touring) { return _buildTouringLayout(context, currentSpeedMph, heading, rideSession, ref, telemetry); } if (isLandscape) { return _buildLandscapeLayout(context, currentSpeedMph, heading, leanAngle, accelG, lateralG, rideSession, ref, telemetry); } return _buildPortraitLayout(context, currentSpeedMph, heading, leanAngle, accelG, lateralG, rideSession, ref, telemetry); }, ), ), ], ), ), ), ); } Widget _buildAlertBanner(RoadAlert alert) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: alert.category.color.withValues(alpha: 0.2), border: Border(bottom: BorderSide(color: alert.category.color, width: 2)), ), child: Row( children: [ Icon(alert.category.icon, color: alert.category.color, size: 28), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( alert.category.displayName.toUpperCase(), style: TextStyle(color: alert.category.color, fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.2), ), Text( '${alert.distanceFromUser?.toStringAsFixed(0) ?? '?'}m Ahead', style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600), ), ], ), ), ], ), ); } Widget _buildPortraitLayout(BuildContext context, double speed, double heading, double lean, double accelG, double lateralG, RideSession session, WidgetRef ref, TelemetrySample? telemetry) { return Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildTopBar(context, heading, session, ref, telemetry), const Spacer(flex: 1), _buildCenterSpeedometer(context, speed), const Spacer(flex: 1), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded(child: GForceVisualizer(accelG: accelG, lateralG: lateralG, session: session)), Expanded(child: LeanVisualizer(lean: lean, session: session)), ], ), const SizedBox(height: 24), _buildTripStatsStrip(context, session), const SizedBox(height: 24), _buildActionButtons(context, session), ], ), ); } Widget _buildLandscapeLayout(BuildContext context, double speed, double heading, double lean, double accelG, double lateralG, RideSession session, WidgetRef ref, TelemetrySample? telemetry) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 8.0), child: Row( children: [ // Left Wing: G-Force + Trip & Top Speed Expanded( flex: 2, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ GForceVisualizer(accelG: accelG, lateralG: lateralG, session: session), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildDataBox(context, 'TRIP', '${session.tripDistanceMiles.toStringAsFixed(2)} MI'), _buildDataBox(context, 'TOP SPD', '${session.topSpeedMph.toStringAsFixed(0)} MPH'), ], ), ], ), ), // Center: Speedometer & Action Buttons Expanded( flex: 3, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildTopBar(context, heading, session, ref, telemetry), _buildCenterSpeedometer(context, speed), _buildActionButtons(context, session), ], ), ), // Right Wing: Lean Angle + Avg Speed & Status Expanded( flex: 2, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ LeanVisualizer(lean: lean, session: session), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildDataBox(context, 'AVG SPD', '${session.averageSpeedMph.toStringAsFixed(1)} MPH'), _buildDataBox(context, 'STATUS', session.state.name.toUpperCase()), ], ), ], ), ), ], ), ); } Widget _buildTopBar(BuildContext context, double heading, RideSession session, WidgetRef ref, TelemetrySample? telemetry) { final headingStr = CompassService.getHeadingString(heading); final gpsAcc = telemetry?.gpsAccuracy; IconData gpsIcon = Icons.gps_off; Color gpsColor = Colors.redAccent; if (gpsAcc != null) { if (gpsAcc <= 15.0) { gpsIcon = Icons.gps_fixed; gpsColor = Colors.greenAccent; } else if (gpsAcc <= 35.0) { gpsIcon = Icons.gps_fixed; gpsColor = Colors.amberAccent; } else { gpsIcon = Icons.gps_not_fixed; gpsColor = Colors.redAccent; } } return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ _buildLayoutSwitcher(ref), const SizedBox(width: 6), Icon(gpsIcon, color: gpsColor, size: 18), const SizedBox(width: 8), Text( headingStr, style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: Colors.cyanAccent), ), const SizedBox(width: 8), Text( '${heading.toStringAsFixed(0)}°', style: const TextStyle(fontSize: 18, color: Colors.white70), ), ], ), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: session.state == RideState.riding ? Colors.red.withValues(alpha: 0.2) : Colors.white10, borderRadius: BorderRadius.circular(16), border: Border.all( color: session.state == RideState.riding ? Colors.redAccent : Colors.white30, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (session.state == RideState.riding) const Padding( padding: EdgeInsets.only(right: 6.0), child: Icon(Icons.circle, color: Colors.redAccent, size: 12), ), Text( session.state == RideState.riding ? 'REC' : session.state.name.toUpperCase(), style: TextStyle( fontWeight: FontWeight.bold, letterSpacing: 1.2, color: session.state == RideState.riding ? Colors.redAccent : Colors.white, ), ), ], ), ), ], ); } Widget _buildCenterSpeedometer(BuildContext context, double speed) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( speed.toStringAsFixed(0), style: const TextStyle( fontSize: 140, height: 1.0, fontWeight: FontWeight.w900, fontFamily: 'Roboto', // Ideally a monospaced/numeric font color: Colors.white, ), ), const Text( 'MPH', style: TextStyle( fontSize: 24, fontWeight: FontWeight.w700, letterSpacing: 4.0, color: Colors.cyanAccent, ), ), ], ); } Widget _buildTripStatsStrip(BuildContext context, RideSession session, {bool isVertical = false}) { final children = [ _buildDataBox(context, 'TRIP', '${session.tripDistanceMiles.toStringAsFixed(2)} MI'), _buildDataBox(context, 'TOP SPEED', '${session.topSpeedMph.toStringAsFixed(0)} MPH'), _buildDataBox(context, 'AVG SPEED', '${session.averageSpeedMph.toStringAsFixed(1)} MPH'), ]; if (isVertical) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: children.map((c) => Padding(padding: const EdgeInsets.only(bottom: 12), child: c)).toList(), ); } return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: children, ); } Widget _buildDataBox(BuildContext context, String label, String value) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(color: Colors.white54, fontSize: 11, letterSpacing: 1.5)), const SizedBox(height: 2), Text(value, style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.w600)), ], ); } Widget _buildActionButtons(BuildContext context, RideSession session) { return Consumer( builder: (context, ref, child) { final notifier = ref.read(rideStateMachineProvider.notifier); if (session.state == RideState.idle || session.state == RideState.finished) { return SizedBox( width: double.infinity, height: 60, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.cyanAccent.withValues(alpha: 0.1), foregroundColor: Colors.cyanAccent, side: const BorderSide(color: Colors.cyanAccent, width: 2), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), onPressed: () => notifier.startRide(), child: const Text('START SESSION', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, letterSpacing: 2)), ), ); } else if (session.state == RideState.riding) { return SizedBox( width: double.infinity, height: 60, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.redAccent.withValues(alpha: 0.1), foregroundColor: Colors.redAccent, side: const BorderSide(color: Colors.redAccent, width: 2), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), onPressed: () => notifier.finishRide(), child: const Text('END SESSION', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, letterSpacing: 2)), ), ); } return const SizedBox(height: 60); }, ); } Widget _buildLayoutSwitcher(WidgetRef ref) { return PopupMenuButton( icon: const Icon(Icons.dashboard_customize, color: Colors.cyanAccent), onSelected: (layout) { ref.read(dashboardConfigProvider.notifier).setLayout(layout); }, itemBuilder: (context) => [ const PopupMenuItem(value: DashboardLayout.sport, child: Text('Sport Layout')), const PopupMenuItem(value: DashboardLayout.touring, child: Text('Touring Layout')), const PopupMenuItem(value: DashboardLayout.minimal, child: Text('Minimal Layout')), ], ); } Widget _buildMinimalLayout(BuildContext context, double speed, {required RideSession session, required WidgetRef ref}) { return Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildLayoutSwitcher(ref), _buildDataBox(context, 'STATUS', session.state.name.toUpperCase()), ], ), const Spacer(), Text( speed.toStringAsFixed(0), style: const TextStyle(fontSize: 200, fontWeight: FontWeight.w900, height: 1.0, color: Colors.white), ), const Text('MPH', style: TextStyle(fontSize: 40, fontWeight: FontWeight.w700, color: Colors.cyanAccent)), const Spacer(), _buildActionButtons(context, session), ], ), ); } Widget _buildTouringLayout(BuildContext context, double speed, double heading, RideSession session, WidgetRef ref, TelemetrySample? telemetry) { return Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ _buildTopBar(context, heading, session, ref, telemetry), const Spacer(), _buildCenterSpeedometer(context, speed), const Spacer(), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white10, borderRadius: BorderRadius.circular(16)), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildDataBox(context, 'TRIP (MI)', session.tripDistanceMiles.toStringAsFixed(1)), _buildDataBox(context, 'AVG (MPH)', session.averageSpeedMph.toStringAsFixed(1)), _buildDataBox(context, 'ELEV (M)', session.elevationGain.toStringAsFixed(0)), _buildDataBox(context, 'MOVING', '${(session.movingTimeSeconds / 60).floor()}m'), ], ), ), const SizedBox(height: 24), _buildActionButtons(context, session), ], ), ); } Widget _buildPermissionBanner(BuildContext context, WidgetRef ref) { return Container( width: double.infinity, color: Colors.amber.shade900.withValues(alpha: 0.95), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ const Icon(Icons.location_off, color: Colors.white, size: 20), const SizedBox(width: 10), const Expanded( child: Text( 'Location permission needed for GPS speedometer.', style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.bold), ), ), const SizedBox(width: 8), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.white, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), onPressed: () async { final locService = ref.read(locationServiceProvider); await locService.requestPermissions(); ref.invalidate(locationPermissionGrantedProvider); }, child: const Text('Enable', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), ), ], ), ); } }