import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../../../core/constants/overlay_constants.dart'; import '../../../core/theme/color_themes.dart'; import '../../shared/speed_display.dart'; import 'speedometer_view_model.dart'; /// Thin UI view for the speedometer — renders speed and compass. /// /// No business logic here. All state comes from [SpeedometerViewModel]. class SpeedometerView extends StatelessWidget { const SpeedometerView({super.key}); @override Widget build(BuildContext context) { return Selector( selector: (_, vm) => vm.displaySpeed, builder: (context, displaySpeed, _) { final vm = context.watch(); final theme = context.select( (theme) => ColorThemes.themes[0], ); // Staleness — UI-layer concern final age = DateTime.now().difference(vm.displayData.timestamp); final showDashes = age >= OverlayConfig.STALENESS_DASH_THRESHOLD; final isDimmed = age >= OverlayConfig.STALENESS_DIM_THRESHOLD; final staleOpacity = isDimmed ? OverlayConfig.STALENESS_DIM_OPACITY : 1.0; final opacity = staleOpacity; final speedText = showDashes ? '--' : displaySpeed.toStringAsFixed(1); return Scaffold( backgroundColor: theme.background, body: SafeArea( child: LayoutBuilder( builder: (context, constraints) { final isLandscape = constraints.maxWidth > constraints.maxHeight; if (isLandscape) { return _buildLandscape(context, theme, speedText, opacity); } else { return _buildPortrait(context, theme, speedText, opacity); } }, ), ), ); }, ); } Widget _buildPortrait(BuildContext context, ColorTheme theme, String speedText, double opacity) { return Column( children: [ Expanded( flex: 65, child: Container( width: double.infinity, color: Colors.transparent, padding: const EdgeInsets.all(4), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible( flex: 7, child: FittedBox( fit: BoxFit.contain, child: SpeedDisplay( fontSize: 200, opacity: opacity, speedText: speedText, timestamp: context.read().displayData.timestamp, ), ), ), const Flexible(flex: 1, child: FittedBox(fit: BoxFit.contain)), Flexible( flex: 2, child: Selector( selector: (_, vm) => vm.displayUnit.label, builder: (context, unitLabel, _) { return GestureDetector( onTap: () { HapticFeedback.lightImpact(); // Unit cycling via ViewModel }, child: FittedBox( fit: BoxFit.contain, child: Text( unitLabel, style: TextStyle( fontSize: 80, color: theme.unitText, fontFamily: 'DIN1451Alt', ), ), ), ); }, ), ), const Flexible(flex: 1, child: FittedBox(fit: BoxFit.contain)), ], ), ), ), Expanded( flex: 32, child: GestureDetector( onTap: () { HapticFeedback.lightImpact(); // Overlay deferred — tap reserved for floating window }, child: Container( width: double.infinity, color: Colors.transparent, padding: const EdgeInsets.all(2), child: _CompassWidget( heading: context.read().displayData.heading, headingText: context.read().displayHeading, isValid: context.read().isHeadingValid, opacity: opacity, theme: theme, ), ), ), ), ], ); } Widget _buildLandscape(BuildContext context, ColorTheme theme, String speedText, double opacity) { return Row( children: [ Expanded( flex: 60, child: Container( height: double.infinity, color: Colors.transparent, padding: const EdgeInsets.all(3), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Flexible(flex: 1, child: FittedBox()), Flexible( flex: 20, child: FittedBox( fit: BoxFit.contain, child: SpeedDisplay( fontSize: 160, opacity: opacity, speedText: speedText, timestamp: context.read().displayData.timestamp, ), ), ), Flexible( flex: 7, child: Selector( selector: (_, vm) => vm.displayUnit.label, builder: (context, unitLabel, _) { return GestureDetector( onTap: () { HapticFeedback.lightImpact(); // Unit cycling via ViewModel }, child: FittedBox( fit: BoxFit.contain, child: Text( unitLabel, style: TextStyle( fontSize: 50, color: theme.unitText, fontFamily: 'DIN1451Alt', ), ), ), ); }, ), ), const Flexible(flex: 5, child: FittedBox()), ], ), ), ), Expanded( flex: 35, child: GestureDetector( onTap: () { HapticFeedback.lightImpact(); // Overlay deferred }, child: Container( height: double.infinity, color: Colors.transparent, padding: const EdgeInsets.all(2), child: _CompassWidget( heading: context.read().displayData.heading, headingText: context.read().displayHeading, isValid: context.read().isHeadingValid, opacity: opacity, theme: theme, ), ), ), ), ], ); } } /// Compass widget — extracted from the old speedometer screen. class _CompassWidget extends StatelessWidget { final double heading; final String headingText; final bool isValid; final double opacity; final ColorTheme theme; const _CompassWidget({ required this.heading, required this.headingText, required this.isValid, required this.opacity, required this.theme, }); @override Widget build(BuildContext context) { final angle = (isValid && heading >= 0 && heading < 360) ? (heading * math.pi / 180.0) : 0.0; return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible( flex: 3, child: Transform.rotate( angle: angle, child: FittedBox( fit: BoxFit.contain, child: Icon( Icons.navigation, size: 80, color: theme.headingText.withValues(alpha: opacity), ), ), ), ), Flexible( flex: 1, child: FittedBox( fit: BoxFit.contain, child: Text( headingText, style: TextStyle( fontSize: 50, color: theme.headingText.withValues(alpha: opacity), fontFamily: 'DIN1451Alt', ), ), ), ), ], ); } }