Fix/landscape dashboard replay batch1 (#85)
* fix(dashboard): resolve landscape clipping and nav rail alignment (#54) * fix(alerts): constrain road incident report modal in landscape (#55) * fix(performance): adapt launch setup layout in landscape (#56) * fix(performance): resolve 68px countdown overflow in landscape (#57) * fix(history): resolve 12px card overflow in landscape (#59) * fix(map): remove duplicate replay action in landscape (#60) * fix(replay): optimize ride replay layout in landscape (#61) * fix(dashboard): prevent Start Session button text clipping in landscape (#54) * fix(dashboard): position lean angle title above rider graphic to prevent overlap (#54) --------- Co-authored-by: Developer <dev@example.com>
This commit is contained in:
parent
1fdca019fb
commit
3f64083bdd
|
|
@ -223,6 +223,8 @@ class HybridRidingActionBar extends ConsumerWidget {
|
|||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
finishLabel ?? 'Finish Ride',
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
style: const TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 15,
|
||||
|
|
@ -238,7 +240,6 @@ class HybridRidingActionBar extends ConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
// Ready / Standby state: Single prominent start button
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
|
|
@ -253,18 +254,24 @@ class HybridRidingActionBar extends ConsumerWidget {
|
|||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
elevation: 0,
|
||||
padding: EdgeInsets.symmetric(horizontal: compact ? 8 : 12),
|
||||
),
|
||||
onPressed: () {
|
||||
notifier.startRide();
|
||||
},
|
||||
icon: Icon(Icons.play_arrow_rounded, size: 26, color: colors.brandCyan),
|
||||
label: const Text(
|
||||
'Start Session',
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
icon: Icon(Icons.play_arrow_rounded, size: compact ? 22 : 26, color: colors.brandCyan),
|
||||
label: const FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
'Start Session',
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -138,45 +138,48 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
|
|||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: NavigationRail(
|
||||
minWidth: 58,
|
||||
groupAlignment: 0.0,
|
||||
selectedIndex: selectedIndex,
|
||||
onDestinationSelected: _onDestinationSelected,
|
||||
backgroundColor: colors.surfaceCanvas,
|
||||
indicatorColor: colors.brandCyan.withValues(alpha: 0.14),
|
||||
selectedIconTheme: IconThemeData(
|
||||
color: colors.brandCyan,
|
||||
size: 21,
|
||||
),
|
||||
unselectedIconTheme: IconThemeData(
|
||||
color: colors.textMuted,
|
||||
size: 19,
|
||||
),
|
||||
selectedLabelTextStyle: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 10.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colors.brandCyan,
|
||||
),
|
||||
unselectedLabelTextStyle: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 9.5,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colors.textMuted,
|
||||
),
|
||||
labelType: NavigationRailLabelType.all,
|
||||
destinations: kAppNavDestinations.map((dest) {
|
||||
return NavigationRailDestination(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
icon: Icon(dest.icon, key: dest.railKey),
|
||||
selectedIcon: dest.selectedIcon != null
|
||||
? Icon(dest.selectedIcon, key: dest.railKey)
|
||||
: null,
|
||||
label: Text(dest.label),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
child: () {
|
||||
final isShortRail = constraints.maxHeight < 400;
|
||||
return NavigationRail(
|
||||
minWidth: isShortRail ? 52 : 58,
|
||||
groupAlignment: isShortRail ? -1.0 : 0.0,
|
||||
selectedIndex: selectedIndex,
|
||||
onDestinationSelected: _onDestinationSelected,
|
||||
backgroundColor: colors.surfaceCanvas,
|
||||
indicatorColor: colors.brandCyan.withValues(alpha: 0.14),
|
||||
selectedIconTheme: IconThemeData(
|
||||
color: colors.brandCyan,
|
||||
size: isShortRail ? 19 : 21,
|
||||
),
|
||||
unselectedIconTheme: IconThemeData(
|
||||
color: colors.textMuted,
|
||||
size: isShortRail ? 17 : 19,
|
||||
),
|
||||
selectedLabelTextStyle: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: isShortRail ? 9.0 : 10.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colors.brandCyan,
|
||||
),
|
||||
unselectedLabelTextStyle: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: isShortRail ? 8.5 : 9.5,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colors.textMuted,
|
||||
),
|
||||
labelType: NavigationRailLabelType.all,
|
||||
destinations: kAppNavDestinations.map((dest) {
|
||||
return NavigationRailDestination(
|
||||
padding: EdgeInsets.symmetric(vertical: isShortRail ? 1 : 4),
|
||||
icon: Icon(dest.icon, key: dest.railKey),
|
||||
selectedIcon: dest.selectedIcon != null
|
||||
? Icon(dest.selectedIcon, key: dest.railKey)
|
||||
: null,
|
||||
label: Text(dest.label),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -233,6 +233,7 @@ class LeanAngleIndicator extends ConsumerWidget {
|
|||
// Header
|
||||
Text(
|
||||
'LEAN ANGLE',
|
||||
key: const ValueKey('lean_angle_title'),
|
||||
style: TextStyle(
|
||||
color: onSurfaceVariant,
|
||||
fontSize: compact ? 9 : 11,
|
||||
|
|
@ -240,22 +241,25 @@ class LeanAngleIndicator extends ConsumerWidget {
|
|||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
SizedBox(height: compact ? 1 : 3),
|
||||
SizedBox(height: compact ? 3 : 6),
|
||||
|
||||
// Motorcycle Leaning Silhouette with Stable Horizon
|
||||
RepaintBoundary(
|
||||
child: SizedBox(
|
||||
key: const ValueKey('lean_angle_rider_graphic'),
|
||||
width: gaugeWidth,
|
||||
height: gaugeHeight,
|
||||
child: CustomPaint(
|
||||
size: Size(gaugeWidth, gaugeHeight),
|
||||
painter: _MotorcycleLeanPainter(
|
||||
leanAngle: displayLean,
|
||||
motorcycleColor: isDark ? const Color(0xFF1E293B) : const Color(0xFFE2E8F0),
|
||||
accentColor: colors.brandCyan,
|
||||
horizonColor: isDark ? Colors.white24 : Colors.black26,
|
||||
tickColor: colors.brandCyan,
|
||||
isDark: isDark,
|
||||
child: ClipRect(
|
||||
child: CustomPaint(
|
||||
size: Size(gaugeWidth, gaugeHeight),
|
||||
painter: _MotorcycleLeanPainter(
|
||||
leanAngle: displayLean,
|
||||
motorcycleColor: isDark ? const Color(0xFF1E293B) : const Color(0xFFE2E8F0),
|
||||
accentColor: colors.brandCyan,
|
||||
horizonColor: isDark ? Colors.white24 : Colors.black26,
|
||||
tickColor: colors.brandCyan,
|
||||
isDark: isDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -426,6 +430,10 @@ class _MotorcycleLeanPainter extends CustomPainter {
|
|||
// 2. Draw Leaning Motorcycle Silhouette
|
||||
canvas.save();
|
||||
canvas.translate(contactPoint.dx, contactPoint.dy);
|
||||
// Scale silhouette to fit comfortably below the top edge of the gauge
|
||||
final availableTopSpace = horizonY - 4.0;
|
||||
final double scale = (availableTopSpace / 47.5).clamp(0.5, 0.95);
|
||||
canvas.scale(scale);
|
||||
final clampedLean = leanAngle.clamp(-60.0, 60.0);
|
||||
canvas.rotate(clampedLean * math.pi / 180.0);
|
||||
|
||||
|
|
|
|||
|
|
@ -326,17 +326,29 @@ class ResponsiveDashboardLayout extends ConsumerWidget {
|
|||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
DashboardLayoutSwitcher(),
|
||||
GPSStatusIndicator(showLabel: false),
|
||||
HeadingIndicator(compact: true),
|
||||
RideStatusChip(),
|
||||
],
|
||||
const FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DashboardLayoutSwitcher(),
|
||||
SizedBox(width: 4),
|
||||
QuickSwarmReportButton(compact: true),
|
||||
SizedBox(width: 4),
|
||||
QuickSquadFeedButton(compact: true),
|
||||
SizedBox(width: 4),
|
||||
GPSStatusIndicator(showLabel: false),
|
||||
SizedBox(width: 4),
|
||||
HeadingIndicator(compact: true),
|
||||
SizedBox(width: 4),
|
||||
RideStatusChip(),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Center(
|
||||
child: SpeedReadout(size: SpeedReadoutSize.compact),
|
||||
const Expanded(
|
||||
child: Center(
|
||||
child: SpeedReadout(size: SpeedReadoutSize.compact),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
|
|
@ -386,7 +398,7 @@ class ResponsiveDashboardLayout extends ConsumerWidget {
|
|||
child: Center(child: SpeedReadout(size: SpeedReadoutSize.hero)),
|
||||
),
|
||||
SizedBox(
|
||||
width: 140,
|
||||
width: 164,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
|
@ -463,6 +475,10 @@ class ResponsiveDashboardLayout extends ConsumerWidget {
|
|||
children: [
|
||||
DashboardLayoutSwitcher(),
|
||||
SizedBox(width: 4),
|
||||
QuickSwarmReportButton(compact: true),
|
||||
SizedBox(width: 4),
|
||||
QuickSquadFeedButton(compact: true),
|
||||
SizedBox(width: 4),
|
||||
GPSStatusIndicator(),
|
||||
SizedBox(width: 4),
|
||||
HeadingIndicator(compact: true),
|
||||
|
|
@ -472,7 +488,9 @@ class ResponsiveDashboardLayout extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
const NavigationManeuverCard(compact: true),
|
||||
const Center(child: SpeedReadout(size: SpeedReadoutSize.compact)),
|
||||
const Expanded(
|
||||
child: Center(child: SpeedReadout(size: SpeedReadoutSize.compact)),
|
||||
),
|
||||
const RideActionButton(compact: true),
|
||||
const SizedBox(height: 4),
|
||||
const PerformanceLaunchShortcut(compact: true),
|
||||
|
|
|
|||
|
|
@ -126,13 +126,13 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
|||
padding: const EdgeInsets.all(12),
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 600,
|
||||
mainAxisExtent: 175,
|
||||
mainAxisExtent: 196,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
itemCount: rides.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildRideCard(context, rides[index], colors);
|
||||
return _buildRideCard(context, rides[index], colors, isGrid: true);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -154,7 +154,12 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildRideCard(BuildContext context, Ride ride, MotorSignalThemeColors colors) {
|
||||
Widget _buildRideCard(
|
||||
BuildContext context,
|
||||
Ride ride,
|
||||
MotorSignalThemeColors colors, {
|
||||
bool isGrid = false,
|
||||
}) {
|
||||
final bikeName = (ride.motorcycleYear != null &&
|
||||
ride.motorcycleMake != null &&
|
||||
ride.motorcycleModel != null)
|
||||
|
|
@ -169,7 +174,7 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
|||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
margin: isGrid ? EdgeInsets.zero : const EdgeInsets.symmetric(vertical: 6),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: colors.outlineVariant),
|
||||
|
|
|
|||
|
|
@ -995,8 +995,8 @@ class _MapScreenState extends ConsumerState<MapScreen> {
|
|||
),
|
||||
),
|
||||
|
||||
// Bottom Center: FIRE OFF REPLAY floating button
|
||||
if (_routePoints.isNotEmpty)
|
||||
// Bottom Center: FIRE OFF REPLAY floating button (portrait only; in landscape play_replay_appbar_button is used)
|
||||
if (_routePoints.isNotEmpty && !responsive.isLandscape)
|
||||
Positioned(
|
||||
bottom: 24,
|
||||
left: 20,
|
||||
|
|
|
|||
|
|
@ -182,21 +182,23 @@ class _SetupView extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isShortLandscape = snapshot.isLandscape && snapshot.isShortViewport;
|
||||
final isLandscape = snapshot.isLandscape;
|
||||
|
||||
if (isShortLandscape) {
|
||||
if (isLandscape) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildModeSelector(context),
|
||||
const SizedBox(height: 12),
|
||||
_buildTargetSelector(context),
|
||||
_buildModeSelector(context, isLandscape: true),
|
||||
const SizedBox(height: 8),
|
||||
_buildTargetSelector(context, isLandscape: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -204,12 +206,13 @@ class _SetupView extends StatelessWidget {
|
|||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildExplanationCard(context),
|
||||
const SizedBox(height: 12),
|
||||
_buildArmButton(context),
|
||||
_buildExplanationCard(context, isLandscape: true),
|
||||
const SizedBox(height: 10),
|
||||
_buildArmButton(context, isLandscape: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -236,7 +239,7 @@ class _SetupView extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildModeSelector(BuildContext context) {
|
||||
Widget _buildModeSelector(BuildContext context, {bool isLandscape = false}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
@ -244,13 +247,13 @@ class _SetupView extends StatelessWidget {
|
|||
'START MODE',
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 12,
|
||||
fontSize: isLandscape ? 11 : 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.0,
|
||||
color: isDark ? Colors.white60 : Colors.black54,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(height: isLandscape ? 6 : 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
|
|
@ -263,6 +266,7 @@ class _SetupView extends StatelessWidget {
|
|||
onTap: () => onModeChanged(PerformanceLaunchMode.auto),
|
||||
colors: colors,
|
||||
isDark: isDark,
|
||||
isLandscape: isLandscape,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
|
@ -276,6 +280,7 @@ class _SetupView extends StatelessWidget {
|
|||
onTap: () => onModeChanged(PerformanceLaunchMode.countdown),
|
||||
colors: colors,
|
||||
isDark: isDark,
|
||||
isLandscape: isLandscape,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -284,7 +289,7 @@ class _SetupView extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildTargetSelector(BuildContext context) {
|
||||
Widget _buildTargetSelector(BuildContext context, {bool isLandscape = false}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
@ -292,13 +297,13 @@ class _SetupView extends StatelessWidget {
|
|||
'BENCHMARK TARGET',
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 12,
|
||||
fontSize: isLandscape ? 11 : 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.0,
|
||||
color: isDark ? Colors.white60 : Colors.black54,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(height: isLandscape ? 6 : 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
|
|
@ -311,6 +316,7 @@ class _SetupView extends StatelessWidget {
|
|||
onTap: () => onTargetChanged(PerformanceLaunchTarget.sixty),
|
||||
colors: colors,
|
||||
isDark: isDark,
|
||||
isLandscape: isLandscape,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
|
@ -324,6 +330,7 @@ class _SetupView extends StatelessWidget {
|
|||
onTap: () => onTargetChanged(PerformanceLaunchTarget.hundred),
|
||||
colors: colors,
|
||||
isDark: isDark,
|
||||
isLandscape: isLandscape,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -332,13 +339,13 @@ class _SetupView extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildExplanationCard(BuildContext context) {
|
||||
Widget _buildExplanationCard(BuildContext context, {bool isLandscape = false}) {
|
||||
final text = selectedMode == PerformanceLaunchMode.auto
|
||||
? 'Auto Launch:\nTiming begins immediately when MotorSignal detects validated motorcycle movement (≥ 1.0 MPH).'
|
||||
: 'Countdown:\nMonotonic 3 → 2 → 1 → GO cadence. Launch Reaction is measured from the scheduled GO instant.';
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
padding: EdgeInsets.all(isLandscape ? 10 : 14),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
|
|
@ -349,17 +356,17 @@ class _SetupView extends StatelessWidget {
|
|||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 20,
|
||||
size: isLandscape ? 18 : 20,
|
||||
color: colors.velocityBlue,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
SizedBox(width: isLandscape ? 8 : 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontBody,
|
||||
fontSize: 13,
|
||||
height: 1.35,
|
||||
fontSize: isLandscape ? 11.5 : 13,
|
||||
height: 1.3,
|
||||
color: isDark ? Colors.white70 : Colors.black87,
|
||||
),
|
||||
),
|
||||
|
|
@ -369,15 +376,16 @@ class _SetupView extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildArmButton(BuildContext context) {
|
||||
Widget _buildArmButton(BuildContext context, {bool isLandscape = false}) {
|
||||
return SizedBox(
|
||||
height: 52,
|
||||
height: isLandscape ? 44 : 52,
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
key: const ValueKey('btn_arm_launch'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colors.launchGreen,
|
||||
foregroundColor: Colors.black,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 2,
|
||||
),
|
||||
onPressed: onArm,
|
||||
|
|
@ -385,7 +393,7 @@ class _SetupView extends StatelessWidget {
|
|||
'Arm Launch',
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 16,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
|
|
@ -403,6 +411,7 @@ class _SelectionButton extends StatelessWidget {
|
|||
final VoidCallback onTap;
|
||||
final MotorSignalThemeColors colors;
|
||||
final bool isDark;
|
||||
final bool isLandscape;
|
||||
|
||||
const _SelectionButton({
|
||||
super.key,
|
||||
|
|
@ -413,6 +422,7 @@ class _SelectionButton extends StatelessWidget {
|
|||
required this.onTap,
|
||||
required this.colors,
|
||||
required this.isDark,
|
||||
this.isLandscape = false,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -428,7 +438,10 @@ class _SelectionButton extends StatelessWidget {
|
|||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isLandscape ? 8 : 12,
|
||||
vertical: isLandscape ? 8 : 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
|
|
@ -439,27 +452,31 @@ class _SelectionButton extends StatelessWidget {
|
|||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 22,
|
||||
size: isLandscape ? 18 : 22,
|
||||
color: isSelected ? colors.launchGreen : (isDark ? Colors.white70 : Colors.black54),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(height: isLandscape ? 4 : 8),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 15,
|
||||
fontSize: isLandscape ? 13 : 15,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontBody,
|
||||
fontSize: 11,
|
||||
fontSize: isLandscape ? 10 : 11,
|
||||
color: isDark ? Colors.white60 : Colors.black54,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -508,44 +525,48 @@ class _WaitingView extends ConsumerWidget {
|
|||
statusColor = colors.velocityBlue;
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(statusIcon, size: 54, color: statusColor),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
statusText,
|
||||
key: const ValueKey('waiting_status_text'),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 1.0,
|
||||
color: statusColor,
|
||||
final isLandscape = snapshot.isLandscape;
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
padding: EdgeInsets.all(isLandscape ? 16 : 24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(statusIcon, size: isLandscape ? 36 : 54, color: statusColor),
|
||||
SizedBox(height: isLandscape ? 10 : 20),
|
||||
Text(
|
||||
statusText,
|
||||
key: const ValueKey('waiting_status_text'),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: isLandscape ? 15 : 18,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 1.0,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Current speed: ${speed.toStringAsFixed(1)} MPH',
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontTelemetry,
|
||||
fontSize: 14,
|
||||
color: isDark ? Colors.white70 : Colors.black54,
|
||||
SizedBox(height: isLandscape ? 6 : 12),
|
||||
Text(
|
||||
'Current speed: ${speed.toStringAsFixed(1)} MPH',
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontTelemetry,
|
||||
fontSize: isLandscape ? 12 : 14,
|
||||
color: isDark ? Colors.white70 : Colors.black54,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
OutlinedButton(
|
||||
key: const ValueKey('btn_cancel_waiting'),
|
||||
onPressed: () {
|
||||
ref.read(performanceLaunchProvider.notifier).reset();
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
SizedBox(height: isLandscape ? 16 : 32),
|
||||
OutlinedButton(
|
||||
key: const ValueKey('btn_cancel_waiting'),
|
||||
onPressed: () {
|
||||
ref.read(performanceLaunchProvider.notifier).reset();
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -570,108 +591,118 @@ class _ReadyView extends ConsumerWidget {
|
|||
final mode = ref.watch(performanceLaunchProvider.select((s) => s.mode));
|
||||
final target = ref.watch(performanceLaunchProvider.select((s) => s.target));
|
||||
final isCountdown = mode == PerformanceLaunchMode.countdown;
|
||||
final isLandscape = snapshot.isLandscape;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.launchGreen.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: colors.launchGreen, width: 2.0),
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isLandscape ? 32 : 20,
|
||||
vertical: isLandscape ? 8 : 16,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isLandscape ? 16 : 24,
|
||||
vertical: isLandscape ? 4 : 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.launchGreen.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: colors.launchGreen, width: 2.0),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: colors.launchGreen, size: isLandscape ? 20 : 28),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'READY',
|
||||
key: const ValueKey('ready_status_badge'),
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: isLandscape ? 20 : 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 2.0,
|
||||
color: colors.launchGreen,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: colors.launchGreen, size: 28),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'READY',
|
||||
key: const ValueKey('ready_status_badge'),
|
||||
),
|
||||
SizedBox(height: isLandscape ? 8 : 20),
|
||||
Text(
|
||||
target.label,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: isLandscape ? 18 : 22,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
),
|
||||
SizedBox(height: isLandscape ? 4 : 10),
|
||||
Text(
|
||||
isCountdown
|
||||
? 'Tap below to initiate countdown'
|
||||
: 'Accelerate when ready',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontBody,
|
||||
fontSize: isLandscape ? 13 : 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isDark ? Colors.white70 : Colors.black54,
|
||||
),
|
||||
),
|
||||
SizedBox(height: isLandscape ? 12 : 40),
|
||||
if (isCountdown) ...[
|
||||
SizedBox(
|
||||
height: isLandscape ? 44 : 56,
|
||||
child: ElevatedButton(
|
||||
key: const ValueKey('btn_start_countdown'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colors.velocityBlue,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
elevation: 4,
|
||||
),
|
||||
onPressed: () {
|
||||
ref.read(performanceLaunchProvider.notifier).startCountdown();
|
||||
},
|
||||
child: const Text(
|
||||
'Start Countdown',
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 2.0,
|
||||
color: colors.launchGreen,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
target.label,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
isCountdown
|
||||
? 'Tap below to initiate countdown'
|
||||
: 'Accelerate when ready',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontBody,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isDark ? Colors.white70 : Colors.black54,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
if (isCountdown) ...[
|
||||
SizedBox(
|
||||
height: 56,
|
||||
child: ElevatedButton(
|
||||
key: const ValueKey('btn_start_countdown'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colors.velocityBlue,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
elevation: 4,
|
||||
),
|
||||
),
|
||||
SizedBox(height: isLandscape ? 8 : 16),
|
||||
],
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
ref.read(performanceLaunchProvider.notifier).startCountdown();
|
||||
ref.read(performanceLaunchProvider.notifier).reset();
|
||||
},
|
||||
child: const Text(
|
||||
'Start Countdown',
|
||||
child: Text(
|
||||
'Cancel & Change Setup',
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
color: isDark ? Colors.white60 : Colors.black54,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
ref.read(performanceLaunchProvider.notifier).reset();
|
||||
},
|
||||
child: Text(
|
||||
'Cancel & Change Setup',
|
||||
style: TextStyle(
|
||||
color: isDark ? Colors.white60 : Colors.black54,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import '../../database/database.dart';
|
|||
import '../../services/map/map_provider_interface.dart';
|
||||
import '../../services/map/unified_map_view.dart';
|
||||
import '../../core/theme/theme_extensions.dart';
|
||||
import '../../core/responsive/responsive_layout.dart';
|
||||
|
||||
/// Corner performance metrics and apex grading for forensic telemetry replay.
|
||||
class CornerAnalysis {
|
||||
|
|
@ -474,117 +475,120 @@ class _ReplayScreenState extends ConsumerState<ReplayScreen> {
|
|||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Map View (with multi-color lean heatmap and telemetry HUD overlay)
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Stack(
|
||||
children: [
|
||||
UnifiedMapView(
|
||||
center: currentPos,
|
||||
zoom: _zoom,
|
||||
markers: allMarkers,
|
||||
polylines: polylines,
|
||||
isFollowModeOverride: _isFollowingRider,
|
||||
onUserInteraction: () {
|
||||
if (_isFollowingRider) {
|
||||
setState(() => _isFollowingRider = false);
|
||||
}
|
||||
},
|
||||
),
|
||||
// Map Navigation Controls (Zoom In, Zoom Out, Recenter)
|
||||
Positioned(
|
||||
right: 14,
|
||||
bottom: 14,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!_isFollowingRider) ...[
|
||||
_ReplayMapButton(
|
||||
key: const ValueKey('replay_recenter_button'),
|
||||
icon: Icons.my_location,
|
||||
tooltip: 'Recenter on Rider',
|
||||
color: colors.cyberCyan,
|
||||
backgroundColor: colors.cyberCyan.withValues(alpha: 0.25),
|
||||
onTap: () => setState(() => _isFollowingRider = true),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
body: ResponsiveLayout(
|
||||
builder: (context, snapshot) {
|
||||
final isLandscape = snapshot.isLandscape;
|
||||
|
||||
final mapWidget = Stack(
|
||||
children: [
|
||||
UnifiedMapView(
|
||||
center: currentPos,
|
||||
zoom: _zoom,
|
||||
markers: allMarkers,
|
||||
polylines: polylines,
|
||||
isFollowModeOverride: _isFollowingRider,
|
||||
onUserInteraction: () {
|
||||
if (_isFollowingRider) {
|
||||
setState(() => _isFollowingRider = false);
|
||||
}
|
||||
},
|
||||
),
|
||||
// Map Navigation Controls (Zoom In, Zoom Out, Recenter)
|
||||
Positioned(
|
||||
right: isLandscape ? 10 : 14,
|
||||
bottom: isLandscape ? 10 : 14,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!_isFollowingRider) ...[
|
||||
_ReplayMapButton(
|
||||
key: const ValueKey('replay_zoom_in_button'),
|
||||
icon: Icons.add,
|
||||
tooltip: 'Zoom In',
|
||||
onTap: () => setState(() => _zoom = (_zoom + 1.0).clamp(3.0, 19.0)),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_ReplayMapButton(
|
||||
key: const ValueKey('replay_zoom_out_button'),
|
||||
icon: Icons.remove,
|
||||
tooltip: 'Zoom Out',
|
||||
onTap: () => setState(() => _zoom = (_zoom - 1.0).clamp(3.0, 19.0)),
|
||||
key: const ValueKey('replay_recenter_button'),
|
||||
icon: Icons.my_location,
|
||||
tooltip: 'Recenter on Rider',
|
||||
color: colors.cyberCyan,
|
||||
backgroundColor: colors.cyberCyan.withValues(alpha: 0.25),
|
||||
isCompact: isLandscape,
|
||||
onTap: () => setState(() => _isFollowingRider = true),
|
||||
),
|
||||
SizedBox(height: isLandscape ? 6 : 8),
|
||||
],
|
||||
_ReplayMapButton(
|
||||
key: const ValueKey('replay_zoom_in_button'),
|
||||
icon: Icons.add,
|
||||
tooltip: 'Zoom In',
|
||||
isCompact: isLandscape,
|
||||
onTap: () => setState(() => _zoom = (_zoom + 1.0).clamp(3.0, 19.0)),
|
||||
),
|
||||
SizedBox(height: isLandscape ? 6 : 8),
|
||||
_ReplayMapButton(
|
||||
key: const ValueKey('replay_zoom_out_button'),
|
||||
icon: Icons.remove,
|
||||
tooltip: 'Zoom Out',
|
||||
isCompact: isLandscape,
|
||||
onTap: () => setState(() => _zoom = (_zoom - 1.0).clamp(3.0, 19.0)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Telemetry Overlay
|
||||
Positioned(
|
||||
top: isLandscape ? 8 : 14,
|
||||
left: isLandscape ? 8 : 14,
|
||||
right: isLandscape ? 8 : 14,
|
||||
child: Center(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildOverlayStat('SPEED', '${currentSample.speed?.toStringAsFixed(0) ?? 0} mph', isLandscape: isLandscape),
|
||||
SizedBox(width: isLandscape ? 6 : 8),
|
||||
_buildOverlayStat('LEAN', '${currentSample.leanAngle?.abs().toStringAsFixed(0) ?? 0}°', isLandscape: isLandscape),
|
||||
SizedBox(width: isLandscape ? 6 : 8),
|
||||
_buildOverlayStat('G', '${currentSample.longitudinalG?.abs().toStringAsFixed(2) ?? 0}G', isLandscape: isLandscape),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Telemetry Overlay
|
||||
Positioned(
|
||||
top: 14,
|
||||
left: 14,
|
||||
right: 14,
|
||||
child: Center(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
),
|
||||
// Heatmap Legend Overlay
|
||||
Positioned(
|
||||
bottom: isLandscape ? 8 : 10,
|
||||
left: isLandscape ? 8 : 14,
|
||||
right: isLandscape ? 60 : 14,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.8),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.white24),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildOverlayStat('SPEED', '${currentSample.speed?.toStringAsFixed(0) ?? 0} mph'),
|
||||
_buildLegendItem(Colors.amberAccent, '≤20°'),
|
||||
const SizedBox(width: 8),
|
||||
_buildOverlayStat('LEAN', '${currentSample.leanAngle?.abs().toStringAsFixed(0) ?? 0}°'),
|
||||
_buildLegendItem(Colors.cyanAccent, '20°-35°'),
|
||||
const SizedBox(width: 8),
|
||||
_buildOverlayStat('G', '${currentSample.longitudinalG?.abs().toStringAsFixed(2) ?? 0}G'),
|
||||
_buildLegendItem(const Color(0xFFFF334B), '>35° Apex'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Heatmap Legend Overlay
|
||||
Positioned(
|
||||
bottom: 10,
|
||||
left: 14,
|
||||
right: 14,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.8),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.white24),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildLegendItem(Colors.amberAccent, '≤20°'),
|
||||
const SizedBox(width: 8),
|
||||
_buildLegendItem(Colors.cyanAccent, '20°-35°'),
|
||||
const SizedBox(width: 8),
|
||||
_buildLegendItem(const Color(0xFFFF334B), '>35° Apex'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Playback Controls Bar
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
final playbackControlsBar = Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isLandscape ? 12 : 16,
|
||||
vertical: isLandscape ? 4 : 8,
|
||||
),
|
||||
color: theme.colorScheme.surface,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
@ -596,10 +600,10 @@ class _ReplayScreenState extends ConsumerState<ReplayScreen> {
|
|||
child: Text(
|
||||
currentSample.timestamp.toLocal().toString().split('.')[0],
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
color: Colors.cyanAccent,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
fontSize: isLandscape ? 11 : 12,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
),
|
||||
),
|
||||
|
|
@ -609,30 +613,42 @@ class _ReplayScreenState extends ConsumerState<ReplayScreen> {
|
|||
'Sample ${_currentIndex + 1} / ${_telemetry.length}',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
fontSize: 11,
|
||||
fontSize: isLandscape ? 10 : 11,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: currentSliderValue,
|
||||
min: 0,
|
||||
max: maxSliderValue,
|
||||
onChanged: _telemetry.length > 1
|
||||
? (val) {
|
||||
setState(() {
|
||||
_currentIndex = val.toInt();
|
||||
});
|
||||
}
|
||||
: null,
|
||||
activeColor: Colors.cyanAccent,
|
||||
SliderTheme(
|
||||
data: isLandscape
|
||||
? SliderTheme.of(context).copyWith(
|
||||
trackHeight: 2,
|
||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 12),
|
||||
)
|
||||
: SliderTheme.of(context),
|
||||
child: Slider(
|
||||
value: currentSliderValue,
|
||||
min: 0,
|
||||
max: maxSliderValue,
|
||||
onChanged: _telemetry.length > 1
|
||||
? (val) {
|
||||
setState(() {
|
||||
_currentIndex = val.toInt();
|
||||
});
|
||||
}
|
||||
: null,
|
||||
activeColor: Colors.cyanAccent,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.restart_alt),
|
||||
visualDensity: isLandscape ? VisualDensity.compact : null,
|
||||
padding: isLandscape ? const EdgeInsets.all(4) : null,
|
||||
constraints: isLandscape ? const BoxConstraints() : null,
|
||||
icon: Icon(Icons.restart_alt, size: isLandscape ? 20 : 24),
|
||||
tooltip: 'Restart',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
|
|
@ -640,35 +656,46 @@ class _ReplayScreenState extends ConsumerState<ReplayScreen> {
|
|||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
SizedBox(width: isLandscape ? 8 : 16),
|
||||
FloatingActionButton.small(
|
||||
onPressed: _togglePlayback,
|
||||
child: Icon(_isPlaying ? Icons.pause : Icons.play_arrow),
|
||||
child: Icon(_isPlaying ? Icons.pause : Icons.play_arrow, size: isLandscape ? 18 : 20),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
SizedBox(width: isLandscape ? 8 : 16),
|
||||
TextButton(
|
||||
style: isLandscape
|
||||
? TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
visualDensity: VisualDensity.compact,
|
||||
)
|
||||
: null,
|
||||
onPressed: _changeSpeed,
|
||||
child: Text(
|
||||
'${_playbackSpeed}x',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontFamily: 'JetBrains Mono'),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fontSize: isLandscape ? 12 : 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Corner Entry / Apex / Exit Breakdown Table
|
||||
Expanded(
|
||||
flex: _corners.isEmpty ? 3 : 4,
|
||||
child: Container(
|
||||
Widget buildCornerTable() {
|
||||
return Container(
|
||||
color: theme.colorScheme.surfaceContainerLow,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isLandscape ? 10 : 14,
|
||||
vertical: isLandscape ? 6 : 8,
|
||||
),
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
|
|
@ -678,7 +705,7 @@ class _ReplayScreenState extends ConsumerState<ReplayScreen> {
|
|||
'CORNER APEX BREAKDOWN & GRADING',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontSize: isLandscape ? 10 : 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
color: colors.cyberCyan,
|
||||
|
|
@ -690,7 +717,7 @@ class _ReplayScreenState extends ConsumerState<ReplayScreen> {
|
|||
Text(
|
||||
_corners.isEmpty ? '' : 'Tap row to seek apex',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontSize: isLandscape ? 9 : 10,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
|
|
@ -712,7 +739,7 @@ class _ReplayScreenState extends ConsumerState<ReplayScreen> {
|
|||
hasLeanData
|
||||
? Icons.straighten_rounded
|
||||
: Icons.sensors_off_rounded,
|
||||
size: 26,
|
||||
size: isLandscape ? 22 : 26,
|
||||
color: hasLeanData
|
||||
? colors.cyberCyan.withValues(alpha: 0.7)
|
||||
: colors.warningAmber.withValues(alpha: 0.8),
|
||||
|
|
@ -724,7 +751,7 @@ class _ReplayScreenState extends ConsumerState<ReplayScreen> {
|
|||
: 'Sensor Fusion Uncalibrated / No Lean Telemetry',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontSize: isLandscape ? 11 : 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.85),
|
||||
fontFamily: 'JetBrains Mono',
|
||||
|
|
@ -732,14 +759,14 @@ class _ReplayScreenState extends ConsumerState<ReplayScreen> {
|
|||
),
|
||||
const SizedBox(height: 4),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 280),
|
||||
constraints: BoxConstraints(maxWidth: isLandscape ? 320 : 280),
|
||||
child: Text(
|
||||
hasLeanData
|
||||
? 'Upright riding recorded. Lean angle remained below 15° throughout this session.'
|
||||
: 'Lean angle was not recorded or IMU was uncalibrated during this session.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontSize: isLandscape ? 10 : 11,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.55),
|
||||
fontFamily: 'Inter',
|
||||
),
|
||||
|
|
@ -771,43 +798,49 @@ class _ReplayScreenState extends ConsumerState<ReplayScreen> {
|
|||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isLandscape ? 10 : 14,
|
||||
vertical: isLandscape ? 6 : 9,
|
||||
),
|
||||
color: isCurrent
|
||||
? colors.cyberCyan.withValues(alpha: 0.12)
|
||||
: Colors.transparent,
|
||||
child: Row(
|
||||
children: [
|
||||
// Corner ID & Direction
|
||||
SizedBox(
|
||||
width: 75,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'T${corner.id}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fontSize: 13,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
children: [
|
||||
// Corner ID & Direction
|
||||
SizedBox(
|
||||
width: isLandscape ? 65 : 75,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'T${corner.id}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fontSize: isLandscape ? 12 : 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
corner.direction == 'Left' ? '↰ L' : '↱ R',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: corner.direction == 'Left'
|
||||
? Colors.blueAccent
|
||||
: Colors.greenAccent,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
corner.direction == 'Left' ? '↰ L' : '↱ R',
|
||||
style: TextStyle(
|
||||
fontSize: isLandscape ? 10 : 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: corner.direction == 'Left'
|
||||
? Colors.blueAccent
|
||||
: Colors.greenAccent,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Entry Speed
|
||||
Expanded(
|
||||
child: Column(
|
||||
const SizedBox(width: 8),
|
||||
// Entry Speed
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
|
|
@ -820,14 +853,16 @@ class _ReplayScreenState extends ConsumerState<ReplayScreen> {
|
|||
),
|
||||
Text(
|
||||
'${corner.entrySpeed.toStringAsFixed(0)} mph',
|
||||
style: const TextStyle(fontSize: 12, fontFamily: 'JetBrains Mono'),
|
||||
style: TextStyle(
|
||||
fontSize: isLandscape ? 11 : 12,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Apex Speed & Lean
|
||||
Expanded(
|
||||
child: Column(
|
||||
SizedBox(width: isLandscape ? 10 : 14),
|
||||
// Apex Speed & Lean
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
|
|
@ -841,7 +876,7 @@ class _ReplayScreenState extends ConsumerState<ReplayScreen> {
|
|||
Text(
|
||||
'${corner.apexSpeed.toStringAsFixed(0)} mph • ${corner.apexLeanAngle.toStringAsFixed(0)}°',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontSize: isLandscape ? 11 : 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colors.cyberCyan,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
|
|
@ -849,10 +884,9 @@ class _ReplayScreenState extends ConsumerState<ReplayScreen> {
|
|||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Exit Speed & Delta
|
||||
Expanded(
|
||||
child: Column(
|
||||
SizedBox(width: isLandscape ? 10 : 14),
|
||||
// Exit Speed & Delta
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
|
|
@ -866,33 +900,34 @@ class _ReplayScreenState extends ConsumerState<ReplayScreen> {
|
|||
Text(
|
||||
'${corner.exitSpeed.toStringAsFixed(0)} mph (${corner.speedDelta >= 0 ? '+' : ''}${corner.speedDelta.toStringAsFixed(0)})',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontSize: isLandscape ? 11 : 12,
|
||||
color: corner.speedDelta >= 0 ? Colors.greenAccent : Colors.orangeAccent,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Grade Badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: corner.getGradeColor().withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: corner.getGradeColor().withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Text(
|
||||
corner.grade,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
color: corner.getGradeColor(),
|
||||
SizedBox(width: isLandscape ? 10 : 14),
|
||||
// Grade Badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: corner.getGradeColor().withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: corner.getGradeColor().withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Text(
|
||||
corner.grade,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
color: corner.getGradeColor(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -901,9 +936,46 @@ class _ReplayScreenState extends ConsumerState<ReplayScreen> {
|
|||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (isLandscape) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: mapWidget,
|
||||
),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Column(
|
||||
children: [
|
||||
playbackControlsBar,
|
||||
Expanded(
|
||||
child: buildCornerTable(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: mapWidget,
|
||||
),
|
||||
playbackControlsBar,
|
||||
Expanded(
|
||||
flex: _corners.isEmpty ? 3 : 4,
|
||||
child: buildCornerTable(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -926,18 +998,28 @@ class _ReplayScreenState extends ConsumerState<ReplayScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildOverlayStat(String label, String value) {
|
||||
Widget _buildOverlayStat(String label, String value, {bool isLandscape = false}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isLandscape ? 10 : 16,
|
||||
vertical: isLandscape ? 4 : 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black87,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(isLandscape ? 8 : 12),
|
||||
border: Border.all(color: Colors.cyanAccent.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 10, color: Colors.white70)),
|
||||
Text(value, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.cyanAccent)),
|
||||
Text(label, style: TextStyle(fontSize: isLandscape ? 8.5 : 10, color: Colors.white70)),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: isLandscape ? 15 : 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.cyanAccent,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -950,6 +1032,7 @@ class _ReplayMapButton extends StatelessWidget {
|
|||
final VoidCallback onTap;
|
||||
final Color? color;
|
||||
final Color? backgroundColor;
|
||||
final bool isCompact;
|
||||
|
||||
const _ReplayMapButton({
|
||||
super.key,
|
||||
|
|
@ -958,10 +1041,13 @@ class _ReplayMapButton extends StatelessWidget {
|
|||
required this.onTap,
|
||||
this.color,
|
||||
this.backgroundColor,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = isCompact ? 36.0 : 48.0;
|
||||
final iconSize = isCompact ? 18.0 : 24.0;
|
||||
return Material(
|
||||
color: backgroundColor ?? Colors.black87,
|
||||
shape: RoundedRectangleBorder(
|
||||
|
|
@ -974,12 +1060,12 @@ class _ReplayMapButton extends StatelessWidget {
|
|||
child: Tooltip(
|
||||
message: tooltip,
|
||||
child: SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
width: size,
|
||||
height: size,
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color ?? Colors.white,
|
||||
size: 24,
|
||||
size: iconSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -152,7 +152,13 @@ class _QuickSwarmReportModalState extends ConsumerState<QuickSwarmReportModal> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final maxHeight = MediaQuery.of(context).size.height * 0.85;
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final insets = MediaQuery.viewInsetsOf(context);
|
||||
final isLandscape = size.width > size.height;
|
||||
final maxHeight = (size.height - insets.bottom) * (isLandscape ? 0.95 : 0.85);
|
||||
final crossAxisCount = size.width >= 720
|
||||
? 4
|
||||
: (size.width >= 480 ? 3 : 2);
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||
|
|
@ -165,7 +171,12 @@ class _QuickSwarmReportModalState extends ConsumerState<QuickSwarmReportModal> {
|
|||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
16,
|
||||
isLandscape ? 8 : 12,
|
||||
16,
|
||||
isLandscape ? 8 : 16,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
|
@ -180,17 +191,17 @@ class _QuickSwarmReportModalState extends ConsumerState<QuickSwarmReportModal> {
|
|||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(height: isLandscape ? 6 : 12),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.campaign, color: AppColors.cyberCyan, size: 24),
|
||||
Icon(Icons.campaign, color: AppColors.cyberCyan, size: isLandscape ? 20 : 24),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'REPORT ROAD INCIDENT',
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 16,
|
||||
fontSize: isLandscape ? 14 : 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.8,
|
||||
color: Colors.white,
|
||||
|
|
@ -199,23 +210,27 @@ class _QuickSwarmReportModalState extends ConsumerState<QuickSwarmReportModal> {
|
|||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white60),
|
||||
padding: isLandscape ? EdgeInsets.zero : const EdgeInsets.all(8.0),
|
||||
constraints: isLandscape ? const BoxConstraints() : null,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'1-tap broadcast to all nearby riders & pack members.',
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontBody,
|
||||
fontSize: 12,
|
||||
color: Colors.white70,
|
||||
if (!isLandscape) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'1-tap broadcast to all nearby riders & pack members.',
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontBody,
|
||||
fontSize: 12,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (_isMovingFast) ...[
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(height: isLandscape ? 6 : 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
|
|
@ -223,7 +238,7 @@ class _QuickSwarmReportModalState extends ConsumerState<QuickSwarmReportModal> {
|
|||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded, color: Colors.amber, size: 18),
|
||||
Icon(Icons.warning_amber_rounded, color: Colors.amber, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
|
|
@ -239,22 +254,23 @@ class _QuickSwarmReportModalState extends ConsumerState<QuickSwarmReportModal> {
|
|||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 14),
|
||||
SizedBox(height: isLandscape ? 8 : 14),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _kReportCategories.length,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 2.2,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisExtent: isLandscape ? 56.0 : 70.0,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final item = _kReportCategories[index];
|
||||
return _buildGridCard(item);
|
||||
return _buildGridCard(item, isLandscape: isLandscape);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
@ -266,14 +282,17 @@ class _QuickSwarmReportModalState extends ConsumerState<QuickSwarmReportModal> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildGridCard(_ReportItem item) {
|
||||
Widget _buildGridCard(_ReportItem item, {bool isLandscape = false}) {
|
||||
return ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: item.color.withValues(alpha: 0.12),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isLandscape ? 6 : 10,
|
||||
vertical: isLandscape ? 4 : 8,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: BorderSide(color: item.color.withValues(alpha: 0.7), width: 1.5),
|
||||
),
|
||||
elevation: 0,
|
||||
|
|
@ -282,14 +301,14 @@ class _QuickSwarmReportModalState extends ConsumerState<QuickSwarmReportModal> {
|
|||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(7),
|
||||
padding: EdgeInsets.all(isLandscape ? 5 : 7),
|
||||
decoration: BoxDecoration(
|
||||
color: item.color.withValues(alpha: 0.22),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(item.icon, color: item.color, size: 22),
|
||||
child: Icon(item.icon, color: item.color, size: isLandscape ? 18 : 22),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: isLandscape ? 6 : 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
@ -299,20 +318,22 @@ class _QuickSwarmReportModalState extends ConsumerState<QuickSwarmReportModal> {
|
|||
item.title,
|
||||
style: TextStyle(
|
||||
fontFamily: AppTypography.fontHeadline,
|
||||
fontSize: 13,
|
||||
fontSize: isLandscape ? 11.5 : 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
item.subtitle,
|
||||
style: const TextStyle(fontSize: 10, color: Colors.white70),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (!isLandscape) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
item.subtitle,
|
||||
style: const TextStyle(fontSize: 10, color: Colors.white70),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -317,6 +317,28 @@ void main() {
|
|||
await tester.pump(const Duration(milliseconds: 100));
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets('BUG-012: HistoryScreen ride cards show all bottom row telemetry without overflow on 800x360 landscape', (tester) async {
|
||||
tester.view.physicalSize = const Size(800, 360);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(() => tester.view.resetPhysicalSize());
|
||||
|
||||
await tester.pumpWidget(buildTestApp(const HistoryScreen()));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.byKey(const ValueKey('history_ride_card_1')), findsOneWidget);
|
||||
expect(find.text('TRIP'), findsOneWidget);
|
||||
expect(find.text('TIME'), findsOneWidget);
|
||||
expect(find.text('TOP SPD'), findsOneWidget);
|
||||
expect(find.text('MAX LEAN'), findsOneWidget);
|
||||
expect(find.text('HARD BRK'), findsOneWidget);
|
||||
expect(find.text('ELEV GAIN'), findsOneWidget);
|
||||
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import 'package:motorcycle_dash/services/alerts/road_alert_models.dart';
|
|||
import 'package:motorcycle_dash/services/alerts/road_alert_service.dart';
|
||||
import 'package:motorcycle_dash/services/location_service.dart';
|
||||
import 'package:motorcycle_dash/services/telemetry_service.dart';
|
||||
import 'package:motorcycle_dash/services/alerts/widgets/quick_swarm_report_modal.dart';
|
||||
import 'package:motorcycle_dash/features/social/feed/widgets/quick_squad_feed_button.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
|
|
@ -166,5 +168,81 @@ void main() {
|
|||
final edgeInsets = paddingWidget.padding as EdgeInsets;
|
||||
expect(edgeInsets.right, greaterThanOrEqualTo(16.0));
|
||||
});
|
||||
|
||||
testWidgets('BUG-007: Short landscape cockpit mount includes incident report and squad buttons with balanced layout', (tester) async {
|
||||
configureViewport(tester, 800, 360);
|
||||
addTearDown(() => tester.view.resetPhysicalSize());
|
||||
|
||||
await tester.pumpWidget(buildTestDashboard());
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.byType(QuickSwarmReportButton), findsOneWidget);
|
||||
expect(find.byType(QuickSquadFeedButton), findsOneWidget);
|
||||
expect(find.byType(SpeedReadout), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('BUG-007 / #54: Start Session button text is fully visible without clipping in minimal landscape (map hidden)', (tester) async {
|
||||
await prefs.setString('dashboardLayout', 'minimal');
|
||||
for (final size in [const Size(640, 360), const Size(800, 360), const Size(840, 390)]) {
|
||||
configureViewport(tester, size.width, size.height);
|
||||
|
||||
await tester.pumpWidget(buildTestDashboard());
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
final startBtnText = find.text('Start Session');
|
||||
expect(startBtnText, findsOneWidget);
|
||||
|
||||
// Verify button text has positive width and fits cleanly
|
||||
final textRenderBox = tester.renderObject(startBtnText) as RenderBox;
|
||||
expect(textRenderBox.size.width, greaterThan(0));
|
||||
expect(textRenderBox.size.height, greaterThan(0));
|
||||
}
|
||||
tester.view.resetPhysicalSize();
|
||||
});
|
||||
|
||||
testWidgets('BUG-007 / #54: Start Session button text is fully visible in sport and touring landscape layouts', (tester) async {
|
||||
for (final layout in ['sport', 'touring']) {
|
||||
await prefs.setString('dashboardLayout', layout);
|
||||
for (final size in [const Size(640, 360), const Size(800, 360), const Size(2000, 900)]) {
|
||||
configureViewport(tester, size.width, size.height);
|
||||
|
||||
await tester.pumpWidget(buildTestDashboard());
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.text('Start Session'), findsOneWidget);
|
||||
}
|
||||
}
|
||||
tester.view.resetPhysicalSize();
|
||||
});
|
||||
|
||||
testWidgets('BUG-007 / #54: LEAN ANGLE title does not overlap rider graphic across landscape viewports', (tester) async {
|
||||
await prefs.setString('dashboardLayout', 'sport');
|
||||
for (final size in [const Size(640, 360), const Size(800, 360), const Size(2000, 900)]) {
|
||||
configureViewport(tester, size.width, size.height);
|
||||
|
||||
await tester.pumpWidget(buildTestDashboard());
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
final titleFinder = find.text('LEAN ANGLE');
|
||||
final graphicFinder = find.byKey(const ValueKey('lean_angle_rider_graphic'));
|
||||
|
||||
expect(titleFinder, findsOneWidget);
|
||||
expect(graphicFinder, findsOneWidget);
|
||||
|
||||
final titleRect = tester.getRect(titleFinder);
|
||||
final graphicRect = tester.getRect(graphicFinder);
|
||||
|
||||
// Prove no overlap between title rect and rider graphic rect
|
||||
expect(titleRect.overlaps(graphicRect), isFalse, reason: 'Title overlaps rider graphic at ${size.width}x${size.height}');
|
||||
// Prove title is positioned strictly above graphic
|
||||
expect(titleRect.bottom, lessThanOrEqualTo(graphicRect.top), reason: 'Title bottom must be above or at graphic top at ${size.width}x${size.height}');
|
||||
}
|
||||
tester.view.resetPhysicalSize();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:motorcycle_dash/core/theme/theme_extensions.dart';
|
||||
import 'package:motorcycle_dash/features/map/map_screen.dart';
|
||||
import 'package:motorcycle_dash/database/database.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:drift/drift.dart' as drift;
|
||||
|
||||
class _TestHttpOverrides extends HttpOverrides {
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext? context) => _MockHttpClient();
|
||||
}
|
||||
|
||||
class _MockHttpHeaders extends Fake implements HttpHeaders {
|
||||
@override
|
||||
void add(String name, Object value, {bool preserveHeaderCase = false}) {}
|
||||
@override
|
||||
void set(String name, Object value, {bool preserveHeaderCase = false}) {}
|
||||
}
|
||||
|
||||
class _MockHttpClient extends Fake implements HttpClient {
|
||||
@override
|
||||
Future<HttpClientRequest> getUrl(Uri url) async => _MockHttpRequest();
|
||||
|
||||
@override
|
||||
Future<HttpClientRequest> openUrl(String method, Uri url) async => _MockHttpRequest();
|
||||
|
||||
@override
|
||||
void close({bool force = false}) {}
|
||||
}
|
||||
|
||||
class _MockHttpRequest extends Fake implements HttpClientRequest {
|
||||
@override
|
||||
final HttpHeaders headers = _MockHttpHeaders();
|
||||
|
||||
@override
|
||||
bool followRedirects = true;
|
||||
|
||||
@override
|
||||
int maxRedirects = 5;
|
||||
|
||||
@override
|
||||
int contentLength = -1;
|
||||
|
||||
@override
|
||||
Future<HttpClientResponse> close() async => _MockHttpResponse();
|
||||
}
|
||||
|
||||
class _MockHttpResponse extends Fake implements HttpClientResponse {
|
||||
@override
|
||||
int get statusCode => 200;
|
||||
@override
|
||||
int get contentLength => _transparent1x1Png.length;
|
||||
@override
|
||||
HttpClientResponseCompressionState get compressionState =>
|
||||
HttpClientResponseCompressionState.notCompressed;
|
||||
@override
|
||||
StreamSubscription<List<int>> listen(
|
||||
void Function(List<int> event)? onData, {
|
||||
Function? onError,
|
||||
void Function()? onDone,
|
||||
bool? cancelOnError,
|
||||
}) {
|
||||
return Stream<List<int>>.value(_transparent1x1Png).listen(
|
||||
onData,
|
||||
onError: onError,
|
||||
onDone: onDone,
|
||||
cancelOnError: cancelOnError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final _transparent1x1Png = <int>[
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
|
||||
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
||||
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00,
|
||||
0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00,
|
||||
0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49,
|
||||
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
];
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
late AppDatabase db;
|
||||
|
||||
setUp(() async {
|
||||
HttpOverrides.global = _TestHttpOverrides();
|
||||
db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
|
||||
// Insert sample ride and telemetry breadcrumbs
|
||||
await db.into(db.rides).insert(
|
||||
RidesCompanion.insert(
|
||||
id: const drift.Value(1),
|
||||
startTime: DateTime(2026, 1, 1, 10, 0),
|
||||
distanceMiles: const drift.Value(15.2),
|
||||
topSpeedMph: const drift.Value(75.0),
|
||||
movingTime: const drift.Value(900),
|
||||
stoppedTime: const drift.Value(60),
|
||||
),
|
||||
);
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
await db.into(db.telemetrySamples).insert(
|
||||
TelemetrySamplesCompanion.insert(
|
||||
rideId: 1,
|
||||
timestamp: DateTime(2026, 1, 1, 10, 0, i * 5),
|
||||
latitude: drift.Value(37.3861 + (i * 0.002)),
|
||||
longitude: drift.Value(-122.2641 + (i * 0.002)),
|
||||
speed: drift.Value(35.0 + i),
|
||||
leanAngle: drift.Value(12.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
HttpOverrides.global = null;
|
||||
await db.close();
|
||||
});
|
||||
|
||||
Widget buildSubject() {
|
||||
return ProviderScope(
|
||||
overrides: [
|
||||
databaseProvider.overrideWithValue(db),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: ThemeData.dark().copyWith(
|
||||
extensions: [MotorSignalThemeColors.dark],
|
||||
),
|
||||
home: const MapScreen(rideId: 1),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('Portrait: renders both appbar action and floating replay action button', (tester) async {
|
||||
tester.view.physicalSize = const Size(360, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(() => tester.view.resetPhysicalSize());
|
||||
|
||||
await tester.pumpWidget(buildSubject());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const ValueKey('play_replay_appbar_button')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('fire_off_ride_preview_button')), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
});
|
||||
|
||||
testWidgets('Landscape: removes duplicate floating replay action to keep map unobstructed (#60)', (tester) async {
|
||||
tester.view.physicalSize = const Size(800, 360);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(() => tester.view.resetPhysicalSize());
|
||||
|
||||
await tester.pumpWidget(buildSubject());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Appbar replay action remains accessible
|
||||
expect(find.byKey(const ValueKey('play_replay_appbar_button')), findsOneWidget);
|
||||
// Floating button is hidden in landscape
|
||||
expect(find.byKey(const ValueKey('fire_off_ride_preview_button')), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
});
|
||||
}
|
||||
|
|
@ -118,6 +118,22 @@ void main() {
|
|||
expect(find.text('0–100 MPH'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('BUG-009: Renders cleanly without clipping on 800x360 landscape phone', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget(size: const Size(800, 360)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.byKey(const ValueKey('launch_mode_auto')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('launch_mode_countdown')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('launch_target_60')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('launch_target_100')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('btn_arm_launch')), findsOneWidget);
|
||||
expect(find.text('Auto Launch'), findsOneWidget);
|
||||
expect(find.text('Countdown'), findsOneWidget);
|
||||
expect(find.text('0–60 MPH'), findsOneWidget);
|
||||
expect(find.text('0–100 MPH'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('3. ARM command uses selected configuration and 4. no local fake READY state', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
|
@ -198,6 +214,21 @@ void main() {
|
|||
expect(find.text('Tap below to initiate countdown'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('BUG-010: Countdown READY renders without 68px overflow on 800x360 landscape phone', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget(size: const Size(800, 360)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
stateMachine.arm(mode: PerformanceLaunchMode.countdown, target: PerformanceLaunchTarget.sixty);
|
||||
telemetryController.add(makeSample(speed: 0.0, confidence: 1.0));
|
||||
await tester.pump(const Duration(milliseconds: 20));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.byKey(const ValueKey('ready_status_badge')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('btn_start_countdown')), findsOneWidget);
|
||||
expect(find.text('Start Countdown'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('8. READY survives widget rebuild', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
|
|
|||
|
|
@ -329,5 +329,18 @@ void main() {
|
|||
expect(alerts.reports, isEmpty);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('BUG-008: renders cleanly without overflow on 800x360 landscape phone', (tester) async {
|
||||
tester.view.physicalSize = const Size(800, 360);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(() => tester.view.resetPhysicalSize());
|
||||
|
||||
await open(tester);
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.text('REPORT ROAD INCIDENT'), findsOneWidget);
|
||||
expect(find.text('Police'), findsOneWidget);
|
||||
expect(find.text('Road Hazard'), findsOneWidget);
|
||||
expect(find.text('Weather Hazard'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -165,4 +165,63 @@ void main() {
|
|||
expect(tester.takeException(), isNull);
|
||||
expect(find.text('CORNER APEX BREAKDOWN & GRADING'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('BUG-014 (#61): ReplayScreen renders responsive horizontal split in 800x360 landscape without overflow', (tester) async {
|
||||
tester.view.physicalSize = const Size(800, 360);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(() => tester.view.resetPhysicalSize());
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
databaseProvider.overrideWithValue(db),
|
||||
],
|
||||
child: const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ReplayScreen(rideId: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
// Verifies both map and breakdown table are rendered concurrently in viewport
|
||||
expect(find.text('CORNER APEX BREAKDOWN & GRADING'), findsOneWidget);
|
||||
expect(find.text('SPEED'), findsOneWidget);
|
||||
expect(find.text('LEAN'), findsOneWidget);
|
||||
expect(find.text('G'), findsOneWidget);
|
||||
expect(find.byType(Slider), findsOneWidget);
|
||||
expect(find.byType(Row), findsWidgets);
|
||||
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
});
|
||||
|
||||
testWidgets('BUG-014 (#61): ReplayScreen renders without overflow on 640x320 compact landscape', (tester) async {
|
||||
tester.view.physicalSize = const Size(640, 320);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(() => tester.view.resetPhysicalSize());
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
databaseProvider.overrideWithValue(db),
|
||||
],
|
||||
child: const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ReplayScreen(rideId: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.text('CORNER APEX BREAKDOWN & GRADING'), findsOneWidget);
|
||||
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue