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 '../../services/telemetry_service.dart'; import 'dashboard_config.dart'; import 'widgets/g_force_visualizer.dart'; import 'widgets/lean_visualizer.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); 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: LayoutBuilder( builder: (context, constraints) { final isLandscape = constraints.maxWidth > constraints.maxHeight; if (layoutMode == DashboardLayout.minimal) { return _buildMinimalLayout(context, currentSpeedMph, session: rideSession, ref: ref); } if (layoutMode == DashboardLayout.touring) { return _buildTouringLayout(context, currentSpeedMph, heading, rideSession, ref); } if (isLandscape) { return _buildLandscapeLayout(context, currentSpeedMph, heading, leanAngle, accelG, lateralG, rideSession, ref); } return _buildPortraitLayout(context, currentSpeedMph, heading, leanAngle, accelG, lateralG, rideSession, ref); }, ), ), ), ); } Widget _buildPortraitLayout(BuildContext context, double speed, double heading, double lean, double accelG, double lateralG, RideSession session, WidgetRef ref) { return Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildTopBar(context, heading, session, ref), 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) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), child: Row( children: [ // Left Wing Expanded( flex: 2, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ GForceVisualizer(accelG: accelG, lateralG: lateralG, session: session), const SizedBox(height: 16), _buildTripStatsStrip(context, session, isVertical: true), ], ), ), // Center Expanded( flex: 3, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildTopBar(context, heading, session, ref), _buildCenterSpeedometer(context, speed), _buildActionButtons(context, session), ], ), ), // Right Wing Expanded( flex: 2, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ LeanVisualizer(lean: lean, session: session), const SizedBox(height: 16), _buildDataBox(context, 'STATUS', session.state.name.toUpperCase()), ], ), ), ], ), ); } Widget _buildTopBar(BuildContext context, double heading, RideSession session, WidgetRef ref) { final headingStr = CompassService.getHeadingString(heading); return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ _buildLayoutSwitcher(ref), 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) { return Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ _buildTopBar(context, heading, session, ref), 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), ], ), ); } }