MotorSignal-App/lib/features/dashboard/widgets/ride_action_button.dart

89 lines
3.1 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_typography.dart';
import '../../rides/ride_state_machine.dart';
/// Primary start/stop recording control button for the rider dashboard.
///
/// Subscribes strictly to [rideStateMachineProvider.select((s) => s.state)]
/// to prevent rebuilding on high-frequency telemetry samples.
class RideActionButton extends ConsumerWidget {
final bool compact;
const RideActionButton({
super.key,
this.compact = false,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(
rideStateMachineProvider.select((s) => s.state),
);
final isDark = Theme.of(context).brightness == Brightness.dark;
final isRiding = state == RideState.riding;
final Color buttonColor;
final String label;
final IconData icon;
if (isRiding) {
buttonColor = AppColors.raceRed;
label = 'Stop Session';
icon = Icons.stop_circle_outlined;
} else {
buttonColor = isDark ? AppColors.cyberCyan : AppColors.lightPrimary;
label = 'Start Session';
icon = Icons.play_circle_outlined;
}
return SizedBox(
width: double.infinity,
height: compact ? 48.0 : 56.0,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: buttonColor.withValues(alpha: isRiding ? 0.16 : 0.12),
foregroundColor: buttonColor,
side: BorderSide(color: buttonColor, width: 1.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 0,
),
onPressed: () {
final notifier = ref.read(rideStateMachineProvider.notifier);
if (isRiding) {
// RID-2: the Stop action is fire-and-forget. finishRide() retains
// its Future error semantics for callers that deliberately await
// it, so the failure is contained here at the UI boundary using the
// project's existing catchError convention
// (create_group_ride_screen.dart) and its runtimeType-only
// diagnostic convention (account_screen.dart, sync_observer.dart).
// Only the exception class is logged - never its payload - so
// tokens and ride content cannot leak, and output is bounded to one
// line per tap. No success is fabricated, no error silently
// swallowed.
notifier.finishRide().catchError((Object error) {
debugPrint('[RideStop] finish failed class=${error.runtimeType}');
});
} else {
notifier.startRide();
}
},
icon: Icon(icon, size: compact ? 20 : 24, color: buttonColor),
label: Text(
label,
style: TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: compact ? 15 : 18,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
),
),
),
);
}
}