927 lines
34 KiB
Dart
927 lines
34 KiB
Dart
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 '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;
|
||
|
||
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 < 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 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 * 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: 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),
|
||
FittedBox(
|
||
fit: BoxFit.scaleDown,
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
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,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// Center: Speedometer & Controls
|
||
Expanded(
|
||
flex: 44,
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
const FittedBox(
|
||
fit: BoxFit.scaleDown,
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
DashboardLayoutSwitcher(),
|
||
SizedBox(width: 4),
|
||
GPSStatusIndicator(showLabel: false),
|
||
SizedBox(width: 4),
|
||
HeadingIndicator(compact: true),
|
||
SizedBox(width: 4),
|
||
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),
|
||
FittedBox(
|
||
fit: BoxFit.scaleDown,
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
MetricTile(
|
||
label: 'AVG',
|
||
value: session.averageSpeedMph.toStringAsFixed(0),
|
||
unit: 'MPH',
|
||
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(),
|
||
),
|
||
],
|
||
),
|
||
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,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|