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/app_colors.dart'; import '../../../core/theme/theme_extensions.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 'performance_summary.dart'; import 'ride_action_button.dart'; import 'ride_status_chip.dart'; import 'speed_readout.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; return Padding( padding: EdgeInsets.symmetric( horizontal: snapshot.isShortViewport ? 10.0 : 16.0, vertical: snapshot.isShortViewport ? 6.0 : 12.0, ), 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 < 760; 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), 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), Container( padding: EdgeInsets.symmetric( horizontal: 16, vertical: isShortPortrait ? 4 : 10, ), decoration: _boxDecoration(context), 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), ] 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), Container( padding: EdgeInsets.symmetric( horizontal: 8, vertical: isShortPortrait ? 4 : 6, ), decoration: _boxDecoration(context), 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.toStringAsFixed(0), unit: 'M', 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), ] else ...[ // Sport Cockpit Mode if (!isShortPortrait) const Spacer(flex: 1), Flexible( flex: isShortPortrait ? 3 : 4, child: Center( child: Transform.translate( offset: Offset(0, -12), 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), Container( padding: EdgeInsets.symmetric( horizontal: 10, vertical: isShortPortrait ? 4 : 8, ), decoration: _boxDecoration(context), 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 (!isShortPortrait) ...[ const SizedBox(height: 6), const PerformanceLaunchShortcut(), ], ], ], ); }, ); } // -------------------------------------------------------------------------- // 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 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ DashboardLayoutSwitcher(), GPSStatusIndicator(showLabel: false), HeadingIndicator(compact: true), RideStatusChip(), ], ), const 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.toStringAsFixed(0), unit: 'M', 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: 140, 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 Wing: G-Force + Trip Stats Expanded( flex: 28, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const GForceIndicator(compact: true), const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ MetricTile( label: 'TRIP', value: session.tripDistanceMiles.toStringAsFixed(1), unit: 'MI', compact: true, ), MetricTile( label: 'TOP', value: session.topSpeedMph.toStringAsFixed(0), unit: 'MPH', compact: true, ), ], ), ], ), ), // Center: Speedometer & Controls Expanded( flex: 44, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ DashboardLayoutSwitcher(), GPSStatusIndicator(showLabel: false), HeadingIndicator(compact: true), RideStatusChip(), ], ), const Center(child: SpeedReadout(size: SpeedReadoutSize.compact)), const RideActionButton(compact: true), const SizedBox(height: 6), const PerformanceLaunchShortcut(compact: true), ], ), ), // Right Wing: Lean Angle + Avg Speed Expanded( flex: 28, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const LeanAngleIndicator(compact: true), const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ MetricTile( label: 'AVG', value: session.averageSpeedMph.toStringAsFixed(0), unit: 'MPH', compact: true, ), 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(), Container( padding: const EdgeInsets.all(14), decoration: _boxDecoration(context), 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.toStringAsFixed(0), unit: 'M', ), ], ), ], ), ), 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), Container( padding: const EdgeInsets.all(12), decoration: _boxDecoration(context), 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(), ), ], ), 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), Container( padding: const EdgeInsets.all(12), decoration: _boxDecoration(context), 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', ), ), ), ], ), ), ], ), ), ], ); } BoxDecoration _boxDecoration(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return BoxDecoration( color: isDark ? AppColors.darkSurfaceContainer : AppColors.lightSurfaceContainer, borderRadius: BorderRadius.circular(12), border: Border.all( color: isDark ? AppColors.darkOutlineVariant : AppColors.lightOutlineVariant, width: 1.0, ), ); } } /// Fast dashboard entry point for the launch timer used before a run. class PerformanceLaunchShortcut extends StatelessWidget { final bool compact; const PerformanceLaunchShortcut({super.key, this.compact = false}); @override Widget build(BuildContext context) { final colors = context.motorColors; return SizedBox( width: double.infinity, height: compact ? 38 : 42, child: OutlinedButton.icon( onPressed: () => context.push('/launch'), icon: Icon(Icons.rocket_launch, size: compact ? 16 : 18), label: const Text('PERFORMANCE LAUNCH'), style: OutlinedButton.styleFrom( foregroundColor: colors.launchGreen, side: BorderSide(color: colors.launchGreen.withValues(alpha: 0.75)), padding: const EdgeInsets.symmetric(horizontal: 10), textStyle: TextStyle( fontSize: compact ? 10 : 11, fontWeight: FontWeight.w800, letterSpacing: 0.35, ), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), ), ); } }