import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/responsive/viewport_size.dart'; import '../../../core/theme/theme_extensions.dart'; import '../../performance/models/performance_launch_models.dart'; import '../../performance/performance_providers.dart'; import '../../rides/ride_state_machine.dart'; import '../dashboard_config.dart'; import 'dashboard_layout_switcher.dart'; import 'dashboard_map_panel.dart'; import 'g_force_indicator.dart'; import 'gps_status_indicator.dart'; import 'heading_indicator.dart'; import 'lean_angle_indicator.dart'; import 'metric_tile.dart'; import 'navigation_maneuver_card.dart'; import 'performance_summary.dart'; import 'ride_action_button.dart'; import 'ride_status_chip.dart'; import 'speed_readout.dart'; import '../../../core/widgets/hybrid_card.dart'; import '../../../services/alerts/widgets/quick_swarm_report_modal.dart'; import '../../social/feed/widgets/quick_squad_feed_button.dart'; /// The single authoritative responsive layout for the MotorSignal dashboard. /// /// Adapts composition based on available [ResponsiveSnapshot] constraints /// and user-selected [DashboardLayout] mode (Sport / Touring / Minimal). /// /// Does NOT rebuild on 20 Hz telemetry ticks; high-frequency telemetry /// is consumed directly by leaf visualizers. class ResponsiveDashboardLayout extends ConsumerWidget { const ResponsiveDashboardLayout({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final layoutMode = ref.watch(dashboardConfigProvider); final session = ref.watch(rideStateMachineProvider); final snapshot = context.responsive; final double leftPad = snapshot.isLandscape ? 12.0 : (snapshot.isShortViewport ? 10.0 : 16.0); final double rightPad = snapshot.isLandscape ? 20.0 : (snapshot.isShortViewport ? 10.0 : 16.0); final double vertPad = snapshot.isShortViewport ? 6.0 : 12.0; return Padding( padding: EdgeInsets.only( left: leftPad, right: rightPad, top: vertPad, bottom: vertPad, ), child: snapshot.isLandscape ? (snapshot.isShortViewport ? _buildShortLandscapeLayout(context, layoutMode, session) : _buildExpandedLayout(context, layoutMode, session, snapshot)) : _buildCompactPortraitLayout(context, layoutMode, session), ); } // -------------------------------------------------------------------------- // COMPACT PORTRAIT // -------------------------------------------------------------------------- Widget _buildCompactPortraitLayout( BuildContext context, DashboardLayout mode, RideSession session, ) { return LayoutBuilder( builder: (context, constraints) { // Group-ride alerts reduce the usable height above this layout. final isShortPortrait = constraints.maxHeight < 880; final isUltraShortPortrait = constraints.maxHeight < 500; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Top status bar Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Row( mainAxisSize: MainAxisSize.min, children: const [ DashboardLayoutSwitcher(), SizedBox(width: 4), QuickSwarmReportButton(compact: true), SizedBox(width: 4), QuickSquadFeedButton(compact: true), SizedBox(width: 4), GPSStatusIndicator(), SizedBox(width: 8), HeadingIndicator(), ], ), ), ), const SizedBox(width: 4), const FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerRight, child: Row( mainAxisSize: MainAxisSize.min, children: [ PerformanceSummary(), SizedBox(width: 6), RideStatusChip(), ], ), ), ], ), // Body based on mode if (mode == DashboardLayout.minimal) ...[ if (!isShortPortrait) const Spacer(flex: 2) else const Spacer(flex: 1), const Flexible( flex: 6, child: Center(child: SpeedReadout(size: SpeedReadoutSize.hero)), ), if (!isShortPortrait) const Spacer(flex: 2) else const Spacer(flex: 1), HybridCard( padding: EdgeInsets.symmetric( horizontal: 16, vertical: isShortPortrait ? 4 : 10, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Expanded( child: Center( child: MetricTile( label: 'TRIP', value: session.tripDistanceMiles.toStringAsFixed(2), unit: 'MI', compact: isShortPortrait, ), ), ), Expanded( child: Center( child: MetricTile( label: 'STATUS', value: session.state.name.toUpperCase(), compact: isShortPortrait, ), ), ), ], ), ), SizedBox(height: isShortPortrait ? 6 : 16), RideActionButton(compact: isShortPortrait), if (!isUltraShortPortrait) ...[ const SizedBox(height: 6), PerformanceLaunchShortcut(compact: isShortPortrait), ], ] else if (mode == DashboardLayout.touring) ...[ SizedBox(height: isShortPortrait ? 2 : 6), SpeedReadout( size: isShortPortrait ? SpeedReadoutSize.compact : SpeedReadoutSize.compact, ), SizedBox(height: isShortPortrait ? 4 : 8), const Expanded(flex: 5, child: DashboardMapPanel()), SizedBox(height: isShortPortrait ? 4 : 10), HybridCard( padding: EdgeInsets.symmetric( horizontal: 8, vertical: isShortPortrait ? 4 : 6, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Expanded( child: Center( child: MetricTile( label: 'TRIP', value: session.tripDistanceMiles.toStringAsFixed(1), unit: 'MI', compact: true, ), ), ), Expanded( child: Center( child: MetricTile( label: 'AVG', value: session.averageSpeedMph.toStringAsFixed(1), unit: 'MPH', compact: true, ), ), ), Expanded( child: Center( child: MetricTile( label: 'ELEV', value: (session.elevationGain * 3.28084).toStringAsFixed(0), unit: 'FT', compact: true, ), ), ), Expanded( child: Center( child: MetricTile( label: 'MOVING', value: '${(session.movingTimeSeconds / 60).floor()}m', compact: true, ), ), ), ], ), ), SizedBox(height: isShortPortrait ? 4 : 10), RideActionButton(compact: isShortPortrait), if (!isUltraShortPortrait) ...[ const SizedBox(height: 6), PerformanceLaunchShortcut(compact: isShortPortrait), ], ] else ...[ // Sport Cockpit Mode if (!isShortPortrait) const Spacer(flex: 1), Flexible( flex: isShortPortrait ? 3 : 4, child: const Center( child: SpeedReadout(size: SpeedReadoutSize.hero), ), ), if (!isShortPortrait) const Spacer(flex: 1), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Expanded(child: GForceIndicator(compact: isShortPortrait)), Expanded(child: LeanAngleIndicator(compact: isShortPortrait)), ], ), SizedBox(height: isShortPortrait ? 4 : 14), HybridCard( padding: EdgeInsets.symmetric( horizontal: 10, vertical: isShortPortrait ? 4 : 8, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Expanded( child: Center( child: MetricTile( label: 'TRIP', value: session.tripDistanceMiles.toStringAsFixed(2), unit: 'MI', icon: Icons.pin_drop_outlined, compact: isShortPortrait, ), ), ), Expanded( child: Center( child: MetricTile( label: 'TOP SPD', value: session.topSpeedMph.toStringAsFixed(0), unit: 'MPH', icon: Icons.speed_outlined, compact: isShortPortrait, ), ), ), Expanded( child: Center( child: MetricTile( label: 'AVG SPD', value: session.averageSpeedMph.toStringAsFixed(1), unit: 'MPH', icon: Icons.av_timer_outlined, compact: isShortPortrait, ), ), ), ], ), ), SizedBox(height: isShortPortrait ? 4 : 14), RideActionButton(compact: isShortPortrait), if (!isUltraShortPortrait) ...[ const SizedBox(height: 6), PerformanceLaunchShortcut(compact: isShortPortrait), ], ], ], ); }, ); } // -------------------------------------------------------------------------- // SHORT LANDSCAPE (Cockpit Horizontal Mount) // -------------------------------------------------------------------------- Widget _buildShortLandscapeLayout( BuildContext context, DashboardLayout mode, RideSession session, ) { if (mode == DashboardLayout.touring) { return Row( children: [ // Left side: Mode-driven embedded map const Expanded(flex: 55, child: DashboardMapPanel()), const SizedBox(width: 8), // Right side: Cockpit telemetry Expanded( flex: 45, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const FittedBox( fit: BoxFit.scaleDown, child: Row( mainAxisSize: MainAxisSize.min, children: [ DashboardLayoutSwitcher(), SizedBox(width: 4), QuickSwarmReportButton(compact: true), SizedBox(width: 4), QuickSquadFeedButton(compact: true), SizedBox(width: 4), GPSStatusIndicator(showLabel: false), SizedBox(width: 4), HeadingIndicator(compact: true), SizedBox(width: 4), RideStatusChip(), ], ), ), const Expanded( child: Center( child: SpeedReadout(size: SpeedReadoutSize.compact), ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ MetricTile( label: 'TRIP', value: session.tripDistanceMiles.toStringAsFixed(1), unit: 'MI', compact: true, ), MetricTile( label: 'AVG', value: session.averageSpeedMph.toStringAsFixed(0), unit: 'MPH', compact: true, ), MetricTile( label: 'ELEV', value: (session.elevationGain * 3.28084).toStringAsFixed(0), unit: 'FT', compact: true, ), ], ), const RideActionButton(compact: true), ], ), ), ], ); } if (mode == DashboardLayout.minimal) { return Row( children: [ const Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ DashboardLayoutSwitcher(), GPSStatusIndicator(), HeadingIndicator(compact: true), RideStatusChip(), ], ), const Expanded( child: Center(child: SpeedReadout(size: SpeedReadoutSize.hero)), ), SizedBox( width: 164, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ MetricTile( label: 'TRIP', value: session.tripDistanceMiles.toStringAsFixed(1), unit: 'MI', ), const SizedBox(height: 12), const RideActionButton(compact: true), ], ), ), ], ); } // Default: Sport Cockpit Horizontal return Row( children: [ // Left Column: G-Force + Trip Statistics Expanded( flex: 29, child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ const GForceIndicator(compact: true), HybridCard( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), child: FittedBox( fit: BoxFit.scaleDown, child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ MetricTile( label: 'TRIP', value: session.tripDistanceMiles.toStringAsFixed(1), unit: 'MI', compact: true, ), const SizedBox(width: 8), MetricTile( label: 'TOP', value: session.topSpeedMph.toStringAsFixed(0), unit: 'MPH', compact: true, ), const SizedBox(width: 8), MetricTile( label: 'AVG', value: session.averageSpeedMph.toStringAsFixed(0), unit: 'MPH', compact: true, ), ], ), ), ), ], ), ), const SizedBox(width: 8), // Center Column: Primary Riding Controls & Speedometer Expanded( flex: 42, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const FittedBox( fit: BoxFit.scaleDown, child: Row( mainAxisSize: MainAxisSize.min, children: [ DashboardLayoutSwitcher(), SizedBox(width: 4), QuickSwarmReportButton(compact: true), SizedBox(width: 4), QuickSquadFeedButton(compact: true), SizedBox(width: 4), GPSStatusIndicator(), SizedBox(width: 4), HeadingIndicator(compact: true), SizedBox(width: 4), RideStatusChip(), ], ), ), const NavigationManeuverCard(compact: true), const Expanded( child: Center(child: SpeedReadout(size: SpeedReadoutSize.compact)), ), const RideActionButton(compact: true), const SizedBox(height: 4), const PerformanceLaunchShortcut(compact: true), ], ), ), const SizedBox(width: 8), // Right Column: Lean-Angle Telemetry + Dynamics Expanded( flex: 29, child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ const LeanAngleIndicator(compact: true), HybridCard( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), child: FittedBox( fit: BoxFit.scaleDown, child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ MetricTile( label: 'MOVING', value: '${(session.movingTimeSeconds / 60).floor()}m', compact: true, ), const SizedBox(width: 8), MetricTile( label: 'ELEV', value: (session.elevationGain * 3.28084).toStringAsFixed(0), unit: 'FT', compact: true, ), const SizedBox(width: 8), const PerformanceSummary(compact: true), ], ), ), ), ], ), ), ], ); } // -------------------------------------------------------------------------- // MEDIUM / EXPANDED (Tablets and Large Displays) // -------------------------------------------------------------------------- Widget _buildExpandedLayout( BuildContext context, DashboardLayout mode, RideSession session, ResponsiveSnapshot snapshot, ) { if (mode == DashboardLayout.touring) { return Row( children: [ const Expanded(flex: 60, child: DashboardMapPanel()), const SizedBox(width: 16), Expanded( flex: 40, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ DashboardLayoutSwitcher(), GPSStatusIndicator(), HeadingIndicator(), RideStatusChip(), ], ), const Spacer(), const Center( child: SpeedReadout(size: SpeedReadoutSize.regular), ), const Spacer(), HybridCard( padding: const EdgeInsets.all(14), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ MetricTile( label: 'TRIP', value: session.tripDistanceMiles.toStringAsFixed(2), unit: 'MI', ), MetricTile( label: 'TOP SPD', value: session.topSpeedMph.toStringAsFixed(0), unit: 'MPH', ), ], ), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ MetricTile( label: 'AVG SPD', value: session.averageSpeedMph.toStringAsFixed(1), unit: 'MPH', ), MetricTile( label: 'ELEV GAIN', value: (session.elevationGain * 3.28084).toStringAsFixed(0), unit: 'FT', ), ], ), ], ), ), const SizedBox(height: 16), const RideActionButton(), ], ), ), ], ); } // Default Sport Multi-Column return Row( children: [ // Left Wing: G-Force & Records Expanded( flex: 30, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const GForceIndicator(), const SizedBox(height: 16), HybridCard( padding: const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Expanded( child: Center( child: MetricTile( label: 'TRIP', value: session.tripDistanceMiles.toStringAsFixed(2), unit: 'MI', ), ), ), Expanded( child: Center( child: MetricTile( label: 'TOP SPD', value: session.topSpeedMph.toStringAsFixed(0), unit: 'MPH', ), ), ), ], ), ), ], ), ), const SizedBox(width: 16), // Center: Status + Speed + Action Expanded( flex: 40, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Row( mainAxisSize: MainAxisSize.min, children: const [ DashboardLayoutSwitcher(), SizedBox(width: 4), GPSStatusIndicator(), SizedBox(width: 4), HeadingIndicator(), ], ), ), ), const SizedBox(width: 4), const FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerRight, child: RideStatusChip(), ), ], ), const NavigationManeuverCard(compact: true), Center( child: Transform.translate( offset: Offset(0, -20), child: SpeedReadout(size: SpeedReadoutSize.hero), ), ), const RideActionButton(), const SizedBox(height: 6), const PerformanceLaunchShortcut(), ], ), ), const SizedBox(width: 16), // Right Wing: Lean Angle & Averages Expanded( flex: 30, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const LeanAngleIndicator(), const SizedBox(height: 16), HybridCard( padding: const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Expanded( child: Center( child: MetricTile( label: 'AVG SPD', value: session.averageSpeedMph.toStringAsFixed(1), unit: 'MPH', ), ), ), Expanded( child: Center( child: MetricTile( label: 'MOVING', value: '${(session.movingTimeSeconds / 60).floor()}m', ), ), ), ], ), ), ], ), ), ], ); } } /// Fast dashboard entry point for the launch timer used before a run. class PerformanceLaunchShortcut extends ConsumerWidget { final bool compact; const PerformanceLaunchShortcut({super.key, this.compact = false}); @override Widget build(BuildContext context, WidgetRef ref) { final colors = context.motorColors; final theme = Theme.of(context); final onSurface = theme.colorScheme.onSurface; final onSurfaceVariant = theme.colorScheme.onSurfaceVariant; final attempt = ref.watch(performanceLaunchProvider); // Derive canonical user-facing status string final String statusText; final Color statusColor; if (attempt.isFalseStart) { statusText = 'FALSE START'; statusColor = colors.raceRed; } else { switch (attempt.state) { case PerformanceLaunchState.countdown: statusText = 'COUNTDOWN'; statusColor = colors.warningAmber; break; case PerformanceLaunchState.launched: statusText = 'RUNNING'; statusColor = colors.brandBlue; break; case PerformanceLaunchState.targetReached: case PerformanceLaunchState.completed: statusText = 'COMPLETE'; statusColor = colors.launchGreen; break; case PerformanceLaunchState.ready: statusText = 'ARMED'; statusColor = colors.launchGreen; break; case PerformanceLaunchState.aborted: statusText = 'ABORTED'; statusColor = colors.raceRed; break; case PerformanceLaunchState.idle: if (attempt.armedAtMonotonicMs != null) { statusText = 'WAITING'; statusColor = colors.warningAmber; } else { statusText = 'READY'; statusColor = colors.brandBlue; } break; } } return Semantics( button: true, label: 'Performance Launch. Status: $statusText. Modes: 0 to 60 MPH, 0 to 100 MPH. Tap to open launch timer.', child: Material( color: Colors.transparent, child: InkWell( key: const ValueKey('dashboard_performance_launch_card'), onTap: () => context.push('/launch'), borderRadius: BorderRadius.circular(8), child: LayoutBuilder( builder: (context, constraints) { final isUltraNarrow = constraints.maxWidth < 180; return Container( width: double.infinity, padding: EdgeInsets.symmetric( horizontal: isUltraNarrow ? 6 : (compact ? 10 : 14), vertical: compact ? 5 : 7, ), decoration: BoxDecoration( color: colors.surfaceCardPrimary, borderRadius: BorderRadius.circular(10), border: Border.all( color: colors.surfaceBorder, width: 1.0, ), ), child: isUltraNarrow ? Column( mainAxisSize: MainAxisSize.min, children: [ FittedBox( fit: BoxFit.scaleDown, child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.bolt, size: 14, color: statusColor), const SizedBox(width: 4), Text( 'PERFORMANCE LAUNCH', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w900, letterSpacing: 0.4, color: onSurface, ), ), ], ), ), const SizedBox(height: 1), FittedBox( fit: BoxFit.scaleDown, child: Text( '0–60 MPH • 0–100 MPH', style: TextStyle( fontSize: 8, fontWeight: FontWeight.w600, color: onSurfaceVariant, ), ), ), const SizedBox(height: 2), FittedBox( fit: BoxFit.scaleDown, child: Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), decoration: BoxDecoration( color: statusColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(3), border: Border.all(color: statusColor.withValues(alpha: 0.4)), ), child: Text( statusText, style: TextStyle( fontSize: 8, fontWeight: FontWeight.w800, letterSpacing: 0.3, color: statusColor, ), ), ), ), ], ) : Row( children: [ Icon( Icons.bolt, size: compact ? 16 : 18, color: statusColor, ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text( 'PERFORMANCE LAUNCH', style: TextStyle( fontSize: compact ? 10 : 11, fontWeight: FontWeight.w900, letterSpacing: 0.5, color: onSurface, ), ), ), const SizedBox(height: 1), FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text( '0–60 MPH • 0–100 MPH', style: TextStyle( fontSize: compact ? 9 : 10, fontWeight: FontWeight.w600, color: onSurfaceVariant, ), ), ), ], ), ), const SizedBox(width: 6), FittedBox( fit: BoxFit.scaleDown, child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: statusColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(4), border: Border.all(color: statusColor.withValues(alpha: 0.4)), ), child: Text( statusText, style: TextStyle( fontSize: compact ? 8 : 9, fontWeight: FontWeight.w800, letterSpacing: 0.4, color: statusColor, ), ), ), ), const SizedBox(width: 4), Icon( Icons.chevron_right, size: compact ? 16 : 18, color: onSurfaceVariant, ), ], ), ); }, ), ), ), ); } }