96 lines
3.9 KiB
Dart
96 lines
3.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../../../core/theme/app_colors.dart';
|
|
import '../../../core/widgets/hybrid_static_rec_badge.dart';
|
|
import '../../rides/ride_state_machine.dart';
|
|
import '../../../models/recording_health_status.dart';
|
|
|
|
/// Authoritative ride status badge/chip.
|
|
///
|
|
/// Subscribes strictly to
|
|
/// [rideStateMachineProvider.select((s) => (s.state, s.healthStatus))] - the
|
|
/// narrowest selection that can render a truthful status claim. RideState decides
|
|
/// which lifecycle branch applies; the RID-3C-1 health status decides the claim.
|
|
/// Both fields change only on real lifecycle or health transitions, so this still
|
|
/// never rebuilds on 20 Hz telemetry ticks.
|
|
class RideStatusChip extends ConsumerWidget {
|
|
const RideStatusChip({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
// RID-3C-2: RideState decides WHICH lifecycle branch applies; the RID-3C-1
|
|
// health status decides the CLAIM inside an active ride. This widget performs
|
|
// no health calculation of its own - recordingActive is already the product of
|
|
// canReportRecording plus the frozen evidence thresholds, so RECORDING can
|
|
// never be claimed from RideState or service presence alone.
|
|
//
|
|
// The select stays narrow (state + healthStatus only), so the chip still does
|
|
// NOT rebuild on 20 Hz telemetry ticks. healthStatus changes only on real
|
|
// warm-up / degradation / recovery / finalization transitions.
|
|
final (state, health) = ref.watch(
|
|
rideStateMachineProvider.select((s) => (s.state, s.healthStatus)),
|
|
);
|
|
|
|
final isActiveRide = state == RideState.riding;
|
|
final isPaused = state == RideState.paused;
|
|
final claimsRecording =
|
|
isActiveRide && health == RecordingHealthStatus.recordingActive;
|
|
|
|
final theme = Theme.of(context);
|
|
final isDark = theme.brightness == Brightness.dark;
|
|
|
|
final Color? customColor;
|
|
final String label;
|
|
|
|
if (isActiveRide) {
|
|
switch (health) {
|
|
case RecordingHealthStatus.recordingActive:
|
|
// Healthy active recording: uses canonical recStaticGreen (#10B981)
|
|
customColor = null;
|
|
label = 'RECORDING';
|
|
break;
|
|
case RecordingHealthStatus.recordingArmed:
|
|
case RecordingHealthStatus.recordingStarting:
|
|
// Ride exists but required evidence has not completed yet. Neutral /
|
|
// informational; the Stop action remains available elsewhere.
|
|
customColor = isDark ? AppColors.cyberCyan : AppColors.lightPrimary;
|
|
label = 'WARMING UP';
|
|
break;
|
|
case RecordingHealthStatus.recordingFinalizing:
|
|
// RID-3B-1 finalization has priority and is never shown as RECORDING.
|
|
customColor = isDark ? AppColors.cyberCyan : AppColors.lightPrimary;
|
|
label = 'FINALIZING';
|
|
break;
|
|
case RecordingHealthStatus.recordingDegraded:
|
|
case RecordingHealthStatus.recordingFailed:
|
|
// Warning, NOT danger: DEGRADED means the ride is still active and still
|
|
// stoppable - not ended, discarded, failed or service-stopped.
|
|
customColor = AppColors.warningAmber;
|
|
label = 'DEGRADED';
|
|
break;
|
|
case RecordingHealthStatus.recordingComplete:
|
|
// Unreachable alongside an active ride; must not present as recording.
|
|
customColor = null;
|
|
label = 'STANDBY';
|
|
break;
|
|
}
|
|
} else if (isPaused) {
|
|
customColor = null;
|
|
label = 'PAUSED';
|
|
} else if (state == RideState.ready) {
|
|
customColor = isDark ? AppColors.cyberCyan : AppColors.lightPrimary;
|
|
label = 'READY';
|
|
} else {
|
|
customColor = null;
|
|
label = 'STANDBY';
|
|
}
|
|
|
|
return HybridStaticRecBadge(
|
|
isRecording: claimsRecording,
|
|
isPaused: isPaused,
|
|
customLabel: label,
|
|
customColor: customColor,
|
|
);
|
|
}
|
|
}
|