diff --git a/lib/features/garage/add_motorcycle_screen.dart b/lib/features/garage/add_motorcycle_screen.dart index 466613e..2e44641 100644 --- a/lib/features/garage/add_motorcycle_screen.dart +++ b/lib/features/garage/add_motorcycle_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../models/motorcycle_models.dart'; +import '../../core/theme/theme_extensions.dart'; import '../../services/entitlements/entitlement_providers.dart'; import '../../services/entitlements/feature_access_service.dart'; import '../entitlements/widgets/pro_lock_badge.dart'; @@ -194,6 +195,8 @@ class _AddMotorcycleScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final colors = context.motorColors; + return PopScope( canPop: _step == 1, onPopInvokedWithResult: (didPop, result) { @@ -201,7 +204,10 @@ class _AddMotorcycleScreenState extends ConsumerState { }, child: Scaffold( appBar: AppBar( - title: Text(_getStepTitle()), + title: Text( + _getStepTitle(), + style: const TextStyle(fontWeight: FontWeight.bold), + ), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: _goBack, @@ -213,10 +219,10 @@ class _AddMotorcycleScreenState extends ConsumerState { children: [ LinearProgressIndicator( value: _step / 5.0, - backgroundColor: Colors.white10, - valueColor: const AlwaysStoppedAnimation(Colors.cyanAccent), + backgroundColor: colors.outlineVariant, + valueColor: AlwaysStoppedAnimation(colors.cyberCyan), ), - Expanded(child: _buildStepContent()), + Expanded(child: _buildStepContent(colors)), ], ), ), @@ -240,24 +246,25 @@ class _AddMotorcycleScreenState extends ConsumerState { } } - Widget _buildStepContent() { + Widget _buildStepContent(MotorSignalThemeColors colors) { switch (_step) { case 1: - return _buildYearStep(); + return _buildYearStep(colors); case 2: - return _buildMakeStep(); + return _buildMakeStep(colors); case 3: - return _buildModelStep(); + return _buildModelStep(colors); case 4: - return _buildVariantStep(); + return _buildVariantStep(colors); case 5: - return _buildConfirmStep(); + return _buildConfirmStep(colors); default: - return const SizedBox(); + return const SizedBox.shrink(); } } - Widget _buildYearStep() { + Widget _buildYearStep(MotorSignalThemeColors colors) { + final theme = Theme.of(context); return GridView.builder( padding: const EdgeInsets.all(16), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( @@ -269,22 +276,25 @@ class _AddMotorcycleScreenState extends ConsumerState { itemCount: _years.length, itemBuilder: (context, index) { final year = _years[index].year; + final isSelected = _selectedYear == year; return OutlinedButton( style: OutlinedButton.styleFrom( side: BorderSide( - color: _selectedYear == year ? Colors.cyanAccent : Colors.white24, + color: isSelected ? colors.cyberCyan : colors.outlineVariant, + width: isSelected ? 2 : 1, ), - backgroundColor: _selectedYear == year - ? Colors.cyanAccent.withValues(alpha: 0.2) - : null, + backgroundColor: isSelected + ? colors.cyberCyan.withValues(alpha: 0.2) + : colors.surfaceContainer, ), onPressed: () => _selectYear(year), child: Text( '$year', - style: const TextStyle( + style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, - color: Colors.white, + fontFamily: 'JetBrains Mono', + color: isSelected ? colors.cyberCyan : theme.colorScheme.onSurface, ), ), ); @@ -292,11 +302,13 @@ class _AddMotorcycleScreenState extends ConsumerState { ); } - Widget _buildMakeStep() { + Widget _buildMakeStep(MotorSignalThemeColors colors) { final query = _searchController.text.toLowerCase(); final filtered = _makes .where((m) => m.name.toLowerCase().contains(query)) .toList(); + final theme = Theme.of(context); + return Column( children: [ Padding( @@ -305,12 +317,12 @@ class _AddMotorcycleScreenState extends ConsumerState { controller: _searchController, decoration: InputDecoration( hintText: 'Search Manufacturer...', - prefixIcon: const Icon(Icons.search, color: Colors.cyanAccent), + prefixIcon: Icon(Icons.search, color: colors.cyberCyan), filled: true, - fillColor: Colors.white10, + fillColor: colors.surfaceContainer, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), - borderSide: BorderSide.none, + borderSide: BorderSide(color: colors.outlineVariant), ), ), onChanged: (_) => setState(() {}), @@ -321,20 +333,21 @@ class _AddMotorcycleScreenState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: filtered.length, separatorBuilder: (_, _) => - const Divider(height: 1, color: Colors.white10), + Divider(height: 1, color: colors.outlineVariant), itemBuilder: (context, index) { final make = filtered[index]; return ListTile( title: Text( make.name, - style: const TextStyle( + style: TextStyle( fontWeight: FontWeight.w600, - fontSize: 18, + fontSize: 17, + color: theme.colorScheme.onSurface, ), ), - trailing: const Icon( + trailing: Icon( Icons.chevron_right, - color: Colors.white30, + color: theme.colorScheme.onSurface.withValues(alpha: 0.3), ), onTap: () => _selectMake(make), ); @@ -345,11 +358,13 @@ class _AddMotorcycleScreenState extends ConsumerState { ); } - Widget _buildModelStep() { + Widget _buildModelStep(MotorSignalThemeColors colors) { final query = _searchController.text.toLowerCase(); final filtered = _models .where((m) => m.name.toLowerCase().contains(query)) .toList(); + final theme = Theme.of(context); + return Column( children: [ Padding( @@ -358,12 +373,12 @@ class _AddMotorcycleScreenState extends ConsumerState { controller: _searchController, decoration: InputDecoration( hintText: 'Search Model...', - prefixIcon: const Icon(Icons.search, color: Colors.cyanAccent), + prefixIcon: Icon(Icons.search, color: colors.cyberCyan), filled: true, - fillColor: Colors.white10, + fillColor: colors.surfaceContainer, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), - borderSide: BorderSide.none, + borderSide: BorderSide(color: colors.outlineVariant), ), ), onChanged: (_) => setState(() {}), @@ -374,21 +389,27 @@ class _AddMotorcycleScreenState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: filtered.length, separatorBuilder: (_, _) => - const Divider(height: 1, color: Colors.white10), + Divider(height: 1, color: colors.outlineVariant), itemBuilder: (context, index) { final model = filtered[index]; return ListTile( title: Text( model.name, - style: const TextStyle( + style: TextStyle( fontWeight: FontWeight.w600, - fontSize: 18, + fontSize: 17, + color: theme.colorScheme.onSurface, ), ), - subtitle: Text('$_selectedYear ${_selectedMake?.name}'), - trailing: const Icon( + subtitle: Text( + '$_selectedYear ${_selectedMake?.name}', + style: TextStyle( + color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + ), + ), + trailing: Icon( Icons.chevron_right, - color: Colors.white30, + color: theme.colorScheme.onSurface.withValues(alpha: 0.3), ), onTap: () => _selectModel(model), ); @@ -399,7 +420,8 @@ class _AddMotorcycleScreenState extends ConsumerState { ); } - Widget _buildVariantStep() { + Widget _buildVariantStep(MotorSignalThemeColors colors) { + final theme = Theme.of(context); return ListView.separated( padding: const EdgeInsets.all(16), itemCount: _variants.length, @@ -407,14 +429,23 @@ class _AddMotorcycleScreenState extends ConsumerState { itemBuilder: (context, index) { final variant = _variants[index]; return Card( + color: colors.surfaceContainer, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide(color: colors.outlineVariant), + ), child: ListTile( title: Text( variant.name, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 17, + color: theme.colorScheme.onSurface, + ), ), - trailing: const Icon( + trailing: Icon( Icons.check_circle_outline, - color: Colors.cyanAccent, + color: colors.cyberCyan, ), onTap: () => _selectVariant(variant), ), @@ -423,19 +454,21 @@ class _AddMotorcycleScreenState extends ConsumerState { ); } - Widget _buildConfirmStep() { + Widget _buildConfirmStep(MotorSignalThemeColors colors) { final spec = _previewSpec; + final theme = Theme.of(context); + return SingleChildScrollView( - padding: const EdgeInsets.all(24.0), + padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white10, + color: colors.surfaceContainer, borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.cyanAccent, width: 2), + border: Border.all(color: colors.cyberCyan, width: 2), ), child: Column( children: [ @@ -443,16 +476,18 @@ class _AddMotorcycleScreenState extends ConsumerState { year: _selectedYear!, make: _selectedMake!.name, model: _selectedModel!.name, - height: 220, + height: 200, borderRadius: BorderRadius.circular(10), ), const SizedBox(height: 14), Text( '$_selectedYear ${_selectedMake?.name} ${_selectedModel?.name}', textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 24, + style: TextStyle( + fontFamily: 'Sora', + fontSize: 20, fontWeight: FontWeight.bold, + color: theme.colorScheme.onSurface, ), ), if (_selectedVariant?.name != null && @@ -460,23 +495,28 @@ class _AddMotorcycleScreenState extends ConsumerState { const SizedBox(height: 4), Text( _selectedVariant!.name, - style: const TextStyle(color: Colors.white70, fontSize: 16), + style: TextStyle( + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), + fontSize: 15, + ), ), ], ], ), ), - const SizedBox(height: 24), + const SizedBox(height: 20), if (spec != null) ...[ - const Text( + Text( 'SPECIFICATION SUMMARY', style: TextStyle( - color: Colors.cyanAccent, + fontFamily: 'Sora', + color: colors.cyberCyan, fontWeight: FontWeight.bold, - letterSpacing: 1.2, + letterSpacing: 1.0, + fontSize: 13, ), ), - const SizedBox(height: 12), + const SizedBox(height: 10), Row( children: [ Expanded( @@ -485,20 +525,24 @@ class _AddMotorcycleScreenState extends ConsumerState { spec.displacementCc != null ? '${spec.displacementCc!.toStringAsFixed(0)} cc' : '—', + colors, + theme, ), ), - const SizedBox(width: 12), + const SizedBox(width: 10), Expanded( child: _buildSpecChip( 'HORSEPOWER', spec.horsepower != null ? '${spec.horsepower!.toStringAsFixed(0)} HP' : '—', + colors, + theme, ), ), ], ), - const SizedBox(height: 12), + const SizedBox(height: 10), Row( children: [ Expanded( @@ -507,36 +551,43 @@ class _AddMotorcycleScreenState extends ConsumerState { spec.torqueLbFt != null ? '${spec.torqueLbFt!.toStringAsFixed(1)} lb-ft' : '—', + colors, + theme, ), ), - const SizedBox(width: 12), + const SizedBox(width: 10), Expanded( child: _buildSpecChip( 'WET WEIGHT', spec.wetWeightLbs != null ? '${spec.wetWeightLbs!.toStringAsFixed(0)} lbs' : '—', + colors, + theme, ), ), ], ), - const SizedBox(height: 24), + const SizedBox(height: 20), ], TextField( controller: _nicknameController, decoration: InputDecoration( labelText: 'Nickname (Optional)', hintText: 'e.g., Blue Beast, The Track Bike', + filled: true, + fillColor: colors.surfaceContainer, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: colors.outlineVariant), ), - prefixIcon: const Icon(Icons.edit, color: Colors.cyanAccent), + prefixIcon: Icon(Icons.edit, color: colors.cyberCyan), ), ), - const SizedBox(height: 32), + const SizedBox(height: 24), ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: Colors.cyanAccent, + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( @@ -547,9 +598,10 @@ class _AddMotorcycleScreenState extends ConsumerState { child: const Text( 'ADD TO GARAGE', style: TextStyle( - fontSize: 18, + fontFamily: 'Sora', + fontSize: 16, fontWeight: FontWeight.bold, - letterSpacing: 1.5, + letterSpacing: 1.2, ), ), ), @@ -558,32 +610,39 @@ class _AddMotorcycleScreenState extends ConsumerState { ); } - Widget _buildSpecChip(String label, String value) { + Widget _buildSpecChip( + String label, + String value, + MotorSignalThemeColors colors, + ThemeData theme, + ) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.05), + color: colors.surfaceContainer, borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.white12), + border: Border.all(color: colors.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, - style: const TextStyle( - fontSize: 10, - color: Colors.white54, - letterSpacing: 1.0, + style: TextStyle( + fontSize: 9.5, + color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + letterSpacing: 0.8, + fontWeight: FontWeight.w700, ), ), const SizedBox(height: 4), Text( value, - style: const TextStyle( - fontSize: 18, + style: TextStyle( + fontSize: 16, fontWeight: FontWeight.bold, - color: Colors.white, + fontFamily: 'JetBrains Mono', + color: theme.colorScheme.onSurface, ), ), ], @@ -591,3 +650,4 @@ class _AddMotorcycleScreenState extends ConsumerState { ); } } + diff --git a/lib/features/garage/garage_screen.dart b/lib/features/garage/garage_screen.dart index 1c844b7..d8871c5 100644 --- a/lib/features/garage/garage_screen.dart +++ b/lib/features/garage/garage_screen.dart @@ -4,7 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../database/database.dart'; import '../../models/motorcycle_models.dart'; -import '../../core/theme/evolution_theme_config.dart'; +import '../../core/responsive/responsive_layout.dart'; +import '../../core/theme/theme_extensions.dart'; import 'garage_providers.dart'; import 'widgets/motorcycle_photo.dart'; import '../../services/entitlements/entitlement_providers.dart'; @@ -17,32 +18,20 @@ class GarageScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final bikesAsync = ref.watch(garageMotorcyclesStreamProvider); - final themeMode = ref.watch(dashboardThemeModeProvider); + final colors = context.motorColors; return Scaffold( - backgroundColor: themeMode == DashboardThemeMode.evolution - ? Colors.black - : null, appBar: AppBar( - backgroundColor: themeMode == DashboardThemeMode.evolution - ? Colors.transparent - : null, - elevation: 0, - title: Text( + title: const Text( 'My Garage', - style: TextStyle( - fontFamily: themeMode == DashboardThemeMode.evolution - ? 'Sora' - : null, - fontWeight: FontWeight.bold, - ), + style: TextStyle(fontWeight: FontWeight.bold), ), actions: [ IconButton( key: const ValueKey('garage_add_motorcycle_appbar_button'), - icon: const Icon( + icon: Icon( Icons.add_circle_outline, - color: Color(0xFF00E5FF), + color: colors.cyberCyan, ), tooltip: 'Add Motorcycle', onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value), @@ -51,34 +40,50 @@ class GarageScreen extends ConsumerWidget { ), body: bikesAsync.when( loading: () => const Center(child: CircularProgressIndicator()), - error: (err, stack) => - Center(child: Text('Error loading garage: $err')), + error: (err, stack) => Center(child: Text('Error loading garage: $err')), data: (bikes) { - if (bikes.isEmpty) return _buildEmptyState(context); - return ListView.builder( - padding: const EdgeInsets.all(16.0), - itemCount: bikes.length, - itemBuilder: (context, index) { - return _buildMotorcycleCard( - context, - ref, - bikes[index], - themeMode, + if (bikes.isEmpty) return _buildEmptyState(context, colors); + + return ResponsiveLayout( + builder: (context, responsive) { + final isMultiColumn = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape); + + if (isMultiColumn) { + return GridView.builder( + padding: const EdgeInsets.all(16.0), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 550, + mainAxisExtent: 420, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + ), + itemCount: bikes.length, + itemBuilder: (context, index) { + return _buildMotorcycleCard(context, ref, bikes[index], colors, isGrid: true); + }, + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16.0), + itemCount: bikes.length, + itemBuilder: (context, index) { + return _buildMotorcycleCard(context, ref, bikes[index], colors, isGrid: false); + }, ); }, ); }, ), - floatingActionButton: - bikesAsync.value != null && bikesAsync.value!.isNotEmpty + floatingActionButton: bikesAsync.value != null && bikesAsync.value!.isNotEmpty ? FloatingActionButton.extended( key: const ValueKey('garage_add_motorcycle_fab'), - backgroundColor: Colors.cyanAccent, + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, icon: const Icon(Icons.two_wheeler), label: const Text( 'ADD MOTORCYCLE', - style: TextStyle(fontWeight: FontWeight.bold), + style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 0.8), ), onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value), ) @@ -107,34 +112,44 @@ class GarageScreen extends ConsumerWidget { context.push('/garage/add'); } - Widget _buildEmptyState(BuildContext context) { + Widget _buildEmptyState(BuildContext context, MotorSignalThemeColors colors) { + final theme = Theme.of(context); return Center( child: Padding( padding: const EdgeInsets.all(32.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.two_wheeler, size: 80, color: Colors.white24), + Icon( + Icons.two_wheeler, + size: 80, + color: theme.colorScheme.onSurface.withValues(alpha: 0.25), + ), const SizedBox(height: 24), - const Text( + Text( 'YOUR GARAGE IS EMPTY', style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - letterSpacing: 1.5, + fontFamily: 'Sora', + fontSize: 18, + fontWeight: FontWeight.w900, + letterSpacing: 1.2, + color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 12), - const Text( + Text( 'Add your motorcycle to personalize your ride history and performance data.', textAlign: TextAlign.center, - style: TextStyle(color: Colors.white70, height: 1.4), + style: TextStyle( + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), + height: 1.4, + ), ), const SizedBox(height: 32), ElevatedButton.icon( key: const ValueKey('garage_add_motorcycle_empty_button'), style: ElevatedButton.styleFrom( - backgroundColor: Colors.cyanAccent, + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric( horizontal: 24, @@ -164,8 +179,9 @@ class GarageScreen extends ConsumerWidget { BuildContext context, WidgetRef ref, GarageMotorcycle bike, - DashboardThemeMode themeMode, - ) { + MotorSignalThemeColors colors, { + bool isGrid = false, + }) { MotorcycleSpecification? spec; if (bike.specificationJson != null) { try { @@ -178,24 +194,25 @@ class GarageScreen extends ConsumerWidget { final displayName = bike.nickname != null && bike.nickname!.isNotEmpty ? '${bike.nickname} (${bike.year} ${bike.makeName} ${bike.modelName})' : '${bike.year} ${bike.makeName} ${bike.modelName}'; - final isEvolution = themeMode == DashboardThemeMode.evolution; + + final theme = Theme.of(context); return Container( - margin: const EdgeInsets.only(bottom: 16), + margin: EdgeInsets.only(bottom: isGrid ? 0 : 16), decoration: BoxDecoration( - color: isEvolution ? const Color(0xFF0D0D0D) : null, - borderRadius: BorderRadius.circular(isEvolution ? 14 : 12), + color: colors.surfaceContainer, + borderRadius: BorderRadius.circular(14), border: Border.all( color: bike.isDefault - ? const Color(0xFF00E5FF) - : (isEvolution ? const Color(0xFF1F1F1F) : Colors.white12), + ? colors.cyberCyan + : colors.outlineVariant, width: bike.isDefault ? 1.8 : 1.0, ), ), child: Material( color: Colors.transparent, child: InkWell( - borderRadius: BorderRadius.circular(isEvolution ? 14 : 12), + borderRadius: BorderRadius.circular(14), onTap: () => context.push('/garage/${bike.id}'), child: Padding( padding: const EdgeInsets.all(12.0), @@ -206,10 +223,10 @@ class GarageScreen extends ConsumerWidget { year: bike.year, make: bike.makeName, model: bike.modelName, - height: 190, - borderRadius: BorderRadius.circular(isEvolution ? 10 : 8), + height: 150, + borderRadius: BorderRadius.circular(10), ), - const SizedBox(height: 14), + const SizedBox(height: 12), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -223,10 +240,10 @@ class GarageScreen extends ConsumerWidget { child: Text( displayName, style: TextStyle( - fontFamily: isEvolution ? 'Sora' : null, - fontSize: 17, + fontFamily: 'Sora', + fontSize: 16, fontWeight: FontWeight.w800, - color: Colors.white, + color: theme.colorScheme.onSurface, ), ), ), @@ -237,41 +254,35 @@ class GarageScreen extends ConsumerWidget { vertical: 2, ), decoration: BoxDecoration( - color: const Color( - 0xFF00E5FF, - ).withValues(alpha: 0.15), + color: colors.cyberCyan.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(4), border: Border.all( - color: const Color(0xFF00E5FF), + color: colors.cyberCyan, ), ), - child: const Text( + child: Text( 'CURRENT', style: TextStyle( fontFamily: 'Inter', fontSize: 10, fontWeight: FontWeight.w900, - color: Color(0xFF00E5FF), + color: colors.cyberCyan, letterSpacing: 0.5, ), ), ), ], ), - const SizedBox(height: 6), + const SizedBox(height: 4), Text( spec != null ? '${spec.displacementCc?.toStringAsFixed(0) ?? "—"} cc • ${spec.horsepower?.toStringAsFixed(0) ?? "—"} hp' : '${bike.year} • ${bike.makeName}', style: TextStyle( - fontFamily: isEvolution ? 'JetBrains Mono' : null, - color: isEvolution - ? const Color(0xFF00E5FF) - : Colors.white70, - fontSize: 13, - fontWeight: isEvolution - ? FontWeight.w700 - : FontWeight.normal, + fontFamily: 'JetBrains Mono', + color: colors.cyberCyan, + fontSize: 12.5, + fontWeight: FontWeight.w700, ), ), ], @@ -287,15 +298,18 @@ class GarageScreen extends ConsumerWidget { runSpacing: 4, children: [ TextButton.icon( - icon: const Icon(Icons.directions_bike, size: 18), - label: const Text('View Specs & Stats'), + icon: Icon(Icons.directions_bike, size: 18, color: colors.cyberCyan), + label: Text( + 'View Specs & Stats', + style: TextStyle(color: colors.cyberCyan, fontWeight: FontWeight.bold), + ), onPressed: () => context.push('/garage/${bike.id}'), ), if (!bike.isDefault) OutlinedButton( style: OutlinedButton.styleFrom( - foregroundColor: Colors.white, - side: const BorderSide(color: Colors.white30), + foregroundColor: theme.colorScheme.onSurface, + side: BorderSide(color: colors.outlineVariant), ), onPressed: () { ref @@ -321,3 +335,4 @@ class GarageScreen extends ConsumerWidget { ); } } + diff --git a/lib/features/garage/motorcycle_detail_screen.dart b/lib/features/garage/motorcycle_detail_screen.dart index 8c0ba42..2265e0f 100644 --- a/lib/features/garage/motorcycle_detail_screen.dart +++ b/lib/features/garage/motorcycle_detail_screen.dart @@ -4,8 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../database/database.dart'; import '../../models/motorcycle_models.dart'; -import '../../core/theme/evolution_theme_config.dart'; -import 'widgets/evolution_motorcycle_detail_view.dart'; +import '../../core/responsive/responsive_layout.dart'; +import '../../core/theme/theme_extensions.dart'; import 'widgets/motorcycle_photo.dart'; import 'garage_providers.dart'; @@ -18,7 +18,8 @@ class MotorcycleDetailScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final bikesAsync = ref.watch(garageMotorcyclesStreamProvider); final statsAsync = ref.watch(motorcyclePerformanceStatsProvider(motorcycleId)); - final themeMode = ref.watch(dashboardThemeModeProvider); + final colors = context.motorColors; + final theme = Theme.of(context); return bikesAsync.when( loading: () => const Scaffold(body: Center(child: CircularProgressIndicator())), @@ -53,21 +54,15 @@ class MotorcycleDetailScreen extends ConsumerWidget { } catch (_) {} } - if (themeMode == DashboardThemeMode.evolution) { - return EvolutionMotorcycleDetailView( - bike: bike, - spec: spec, - onEdit: () => _showEditNicknameDialog(context, ref, bike), - onDelete: () => _confirmDelete(context, ref, bike), - ); - } - return Scaffold( appBar: AppBar( title: FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, - child: Text('${bike.year} ${bike.makeName} ${bike.modelName}'), + child: Text( + '${bike.year} ${bike.makeName} ${bike.modelName}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), ), actions: [ IconButton( @@ -76,87 +71,154 @@ class MotorcycleDetailScreen extends ConsumerWidget { onPressed: () => _showEditNicknameDialog(context, ref, bike), ), IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.redAccent), + icon: Icon(Icons.delete_outline, color: colors.raceRed), tooltip: 'Delete Motorcycle', - onPressed: () => _confirmDelete(context, ref, bike), + onPressed: () => _confirmDelete(context, ref, bike, colors), ), ], ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildHeaderCard(context, ref, bike, spec), - const SizedBox(height: 24), - _buildMyPerformanceSection(context, statsAsync, spec), - const SizedBox(height: 24), - _buildSpecificationSheet(context, spec), - ], - ), + body: ResponsiveLayout( + builder: (context, responsive) { + final isWide = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape); + + if (isWide) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 4, + child: _buildHeaderCard(context, ref, bike, spec, colors, theme), + ), + const SizedBox(width: 16), + Expanded( + flex: 6, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildMyPerformanceSection(context, statsAsync, spec, colors, theme), + const SizedBox(height: 20), + _buildSpecificationSheet(context, spec, colors, theme), + ], + ), + ), + ], + ), + ); + } + + return SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildHeaderCard(context, ref, bike, spec, colors, theme), + const SizedBox(height: 20), + _buildMyPerformanceSection(context, statsAsync, spec, colors, theme), + const SizedBox(height: 20), + _buildSpecificationSheet(context, spec, colors, theme), + const SizedBox(height: 24), + ], + ), + ); + }, ), ); }, ); } - Widget _buildHeaderCard(BuildContext context, WidgetRef ref, GarageMotorcycle bike, MotorcycleSpecification? spec) { + Widget _buildHeaderCard( + BuildContext context, + WidgetRef ref, + GarageMotorcycle bike, + MotorcycleSpecification? spec, + MotorSignalThemeColors colors, + ThemeData theme, + ) { return Card( - color: const Color(0xFF161616), + elevation: 2, + color: colors.surfaceContainer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), - side: BorderSide(color: bike.isDefault ? Colors.cyanAccent : Colors.white12, width: 2), + side: BorderSide( + color: bike.isDefault ? colors.cyberCyan : colors.outlineVariant, + width: bike.isDefault ? 1.8 : 1.0, + ), ), child: Padding( - padding: const EdgeInsets.all(20.0), + padding: const EdgeInsets.all(16.0), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ MotorcyclePhoto( year: bike.year, make: bike.makeName, model: bike.modelName, - height: 210, + height: 190, borderRadius: BorderRadius.circular(12), showAttribution: true, fit: BoxFit.cover, ), const SizedBox(height: 16), - Row( + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${bike.year}', - style: const TextStyle(fontSize: 16, color: Colors.cyanAccent, fontWeight: FontWeight.bold), - ), - Text( - '${bike.makeName} ${bike.modelName}'.toUpperCase(), - style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w900, letterSpacing: 0.5), - ), - if (bike.nickname != null && bike.nickname!.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text( - '"${bike.nickname}"', - style: const TextStyle(fontSize: 16, fontStyle: FontStyle.italic, color: Colors.amberAccent), - ), - ), - ], + Text( + '${bike.year}', + style: TextStyle( + fontSize: 14, + color: colors.cyberCyan, + fontWeight: FontWeight.bold, + fontFamily: 'JetBrains Mono', ), ), + Text( + '${bike.makeName} ${bike.modelName}'.toUpperCase(), + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w900, + fontFamily: 'Sora', + color: theme.colorScheme.onSurface, + ), + ), + if (bike.nickname != null && bike.nickname!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + '"${bike.nickname}"', + style: TextStyle( + fontSize: 15, + fontStyle: FontStyle.italic, + color: colors.warningAmber, + fontWeight: FontWeight.w600, + ), + ), + ), ], ), const SizedBox(height: 16), - const Divider(color: Colors.white10), + Divider(color: colors.outlineVariant, height: 1), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _buildHeaderStat('DISPLACEMENT', spec?.displacementCc != null ? '${spec!.displacementCc!.toStringAsFixed(0)} cc' : '—'), - _buildHeaderStat('POWER', spec?.horsepower != null ? '${spec!.horsepower!.toStringAsFixed(0)} HP' : '—'), - _buildHeaderStat('TORQUE', spec?.torqueLbFt != null ? '${spec!.torqueLbFt!.toStringAsFixed(1)} lb-ft' : '—'), + _buildHeaderStat( + 'DISPLACEMENT', + spec?.displacementCc != null ? '${spec!.displacementCc!.toStringAsFixed(0)} cc' : '—', + theme, + ), + _buildHeaderStat( + 'POWER', + spec?.horsepower != null ? '${spec!.horsepower!.toStringAsFixed(0)} HP' : '—', + theme, + ), + _buildHeaderStat( + 'TORQUE', + spec?.torqueLbFt != null ? '${spec!.torqueLbFt!.toStringAsFixed(1)} lb-ft' : '—', + theme, + ), ], ), const SizedBox(height: 16), @@ -166,30 +228,46 @@ class MotorcycleDetailScreen extends ConsumerWidget { ? Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( - color: Colors.cyanAccent.withValues(alpha: 0.15), + color: colors.cyberCyan.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.cyanAccent), + border: Border.all(color: colors.cyberCyan), ), - child: const Center( + child: Center( child: Text( 'CURRENT MOTORCYCLE', - style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.5, color: Colors.cyanAccent), + style: TextStyle( + fontFamily: 'Sora', + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + color: colors.cyberCyan, + ), ), ), ) : OutlinedButton( style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), - side: const BorderSide(color: Colors.cyanAccent), - foregroundColor: Colors.cyanAccent, + side: BorderSide(color: colors.cyberCyan), + foregroundColor: colors.cyberCyan, ), onPressed: () { ref.read(garageControllerProvider).setDefaultMotorcycle(bike.id); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Set ${bike.makeName} ${bike.modelName} as current motorcycle.')), + SnackBar( + content: Text( + 'Set ${bike.makeName} ${bike.modelName} as current motorcycle.', + ), + ), ); }, - child: const Text('SET AS CURRENT MOTORCYCLE', style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.2)), + child: const Text( + 'SET AS CURRENT MOTORCYCLE', + style: TextStyle( + fontFamily: 'Sora', + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + ), + ), ), ), ], @@ -198,23 +276,59 @@ class MotorcycleDetailScreen extends ConsumerWidget { ); } - Widget _buildHeaderStat(String label, String value) { - return Column( - children: [ - Text(label, style: const TextStyle(fontSize: 10, color: Colors.white54, letterSpacing: 1.2)), - const SizedBox(height: 4), - Text(value, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)), - ], + Widget _buildHeaderStat(String label, String value, ThemeData theme) { + return Expanded( + child: Column( + children: [ + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + label, + style: TextStyle( + fontSize: 9.5, + color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + letterSpacing: 1.0, + fontWeight: FontWeight.w700, + ), + ), + ), + const SizedBox(height: 4), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + value, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + fontFamily: 'JetBrains Mono', + color: theme.colorScheme.onSurface, + ), + ), + ), + ], + ), ); } - Widget _buildMyPerformanceSection(BuildContext context, AsyncValue statsAsync, MotorcycleSpecification? spec) { + Widget _buildMyPerformanceSection( + BuildContext context, + AsyncValue statsAsync, + MotorcycleSpecification? spec, + MotorSignalThemeColors colors, + ThemeData theme, + ) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( 'MY PERFORMANCE', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.cyanAccent, letterSpacing: 1.2), + style: TextStyle( + fontFamily: 'Sora', + fontSize: 16, + fontWeight: FontWeight.bold, + color: colors.cyberCyan, + letterSpacing: 0.8, + ), ), const SizedBox(height: 12), statsAsync.when( @@ -225,9 +339,16 @@ class MotorcycleDetailScreen extends ConsumerWidget { children: [ Row( children: [ - Expanded(child: _buildPerfTile('TOTAL RIDES', '${stats.rideCount}')), + Expanded(child: _buildPerfTile('TOTAL RIDES', '${stats.rideCount}', colors, theme)), const SizedBox(width: 12), - Expanded(child: _buildPerfTile('TOTAL DISTANCE', '${stats.totalDistanceMiles.toStringAsFixed(1)} MI')), + Expanded( + child: _buildPerfTile( + 'TOTAL DISTANCE', + '${stats.totalDistanceMiles.toStringAsFixed(1)} MI', + colors, + theme, + ), + ), ], ), const SizedBox(height: 12), @@ -237,7 +358,11 @@ class MotorcycleDetailScreen extends ConsumerWidget { child: _buildPerfTile( 'BEST TOP SPEED', '${stats.topSpeedMph.toStringAsFixed(1)} MPH', - published: spec?.claimedTopSpeedMph != null ? '~${spec!.claimedTopSpeedMph!.toStringAsFixed(0)} MPH' : null, + colors, + theme, + published: spec?.claimedTopSpeedMph != null + ? '~${spec!.claimedTopSpeedMph!.toStringAsFixed(0)} MPH' + : null, ), ), const SizedBox(width: 12), @@ -245,7 +370,11 @@ class MotorcycleDetailScreen extends ConsumerWidget { child: _buildPerfTile( 'BEST 0–60', stats.best0to60Ms != null ? '${(stats.best0to60Ms! / 1000.0).toStringAsFixed(2)} s' : '—', - published: spec?.claimedZeroToSixtySec != null ? '~${spec!.claimedZeroToSixtySec!.toStringAsFixed(1)} s' : null, + colors, + theme, + published: spec?.claimedZeroToSixtySec != null + ? '~${spec!.claimedZeroToSixtySec!.toStringAsFixed(1)} s' + : null, ), ), ], @@ -253,9 +382,23 @@ class MotorcycleDetailScreen extends ConsumerWidget { const SizedBox(height: 12), Row( children: [ - Expanded(child: _buildPerfTile('MAX LEAN', '${stats.maxLean.toStringAsFixed(1)}°')), + Expanded( + child: _buildPerfTile( + 'MAX LEAN', + '${stats.maxLean.toStringAsFixed(1)}°', + colors, + theme, + ), + ), const SizedBox(width: 12), - Expanded(child: _buildPerfTile('MAX ACCEL G', '${stats.maxAccelG.toStringAsFixed(2)} G')), + Expanded( + child: _buildPerfTile( + 'MAX ACCEL G', + '${stats.maxAccelG.toStringAsFixed(2)} G', + colors, + theme, + ), + ), ], ), ], @@ -266,35 +409,80 @@ class MotorcycleDetailScreen extends ConsumerWidget { ); } - Widget _buildPerfTile(String label, String value, {String? published}) { + Widget _buildPerfTile( + String label, + String value, + MotorSignalThemeColors colors, + ThemeData theme, { + String? published, + }) { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.05), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.white10), + color: colors.surfaceContainer, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: colors.outlineVariant, width: 1.2), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: const TextStyle(fontSize: 10, color: Colors.white54, letterSpacing: 1.0)), + Text( + label, + style: TextStyle( + fontSize: 10, + color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + fontWeight: FontWeight.w700, + letterSpacing: 0.8, + ), + ), const SizedBox(height: 4), - Text(value, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white)), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + value, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + fontFamily: 'JetBrains Mono', + color: theme.colorScheme.onSurface, + ), + ), + ), if (published != null) ...[ const SizedBox(height: 4), - Text('PUBLISHED: $published', style: const TextStyle(fontSize: 10, color: Colors.cyanAccent)), + Text( + 'PUBLISHED: $published', + style: TextStyle( + fontSize: 9.5, + color: colors.cyberCyan, + fontFamily: 'JetBrains Mono', + ), + ), ], ], ), ); } - Widget _buildSpecificationSheet(BuildContext context, MotorcycleSpecification? spec) { + Widget _buildSpecificationSheet( + BuildContext context, + MotorcycleSpecification? spec, + MotorSignalThemeColors colors, + ThemeData theme, + ) { if (spec == null) { - return const Card( - child: Padding( + return Card( + color: colors.surfaceContainer, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: colors.outlineVariant), + ), + child: const Padding( padding: EdgeInsets.all(16.0), - child: Text('Detailed specifications unavailable for this motorcycle.', style: TextStyle(color: Colors.white54)), + child: Text( + 'Detailed specifications unavailable for this motorcycle.', + style: TextStyle(color: Colors.white54), + ), ), ); } @@ -302,9 +490,15 @@ class MotorcycleDetailScreen extends ConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( 'SPECIFICATIONS', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.cyanAccent, letterSpacing: 1.2), + style: TextStyle( + fontFamily: 'Sora', + fontSize: 16, + fontWeight: FontWeight.bold, + color: colors.cyberCyan, + letterSpacing: 0.8, + ), ), const SizedBox(height: 12), _buildSpecGroup('ENGINE', [ @@ -317,23 +511,23 @@ class MotorcycleDetailScreen extends ConsumerWidget { _SpecRow('Compression Ratio', spec.compressionRatio), _SpecRow('Cooling System', spec.coolingSystem), _SpecRow('Fuel System', spec.fuelSystem), - ]), + ], colors, theme), _buildSpecGroup('PERFORMANCE (PUBLISHED)', [ _SpecRow('Claimed Top Speed', spec.claimedTopSpeedMph != null ? '~${spec.claimedTopSpeedMph!.toStringAsFixed(0)} MPH' : null), _SpecRow('Claimed 0–60 MPH', spec.claimedZeroToSixtySec != null ? '~${spec.claimedZeroToSixtySec!.toStringAsFixed(1)} sec' : null), _SpecRow('Claimed 1/4 Mile', spec.claimedQuarterMileSec != null ? '~${spec.claimedQuarterMileSec!.toStringAsFixed(1)} sec' : null), - ]), + ], colors, theme), _buildSpecGroup('WEIGHT & DIMENSIONS', [ _SpecRow('Wet Weight', spec.wetWeightLbs != null ? '${spec.wetWeightLbs!.toStringAsFixed(0)} lbs' : null), _SpecRow('Dry Weight', spec.dryWeightLbs != null ? '${spec.dryWeightLbs!.toStringAsFixed(0)} lbs' : null), _SpecRow('Seat Height', spec.seatHeightInches != null ? '${spec.seatHeightInches!.toStringAsFixed(1)} in' : null), _SpecRow('Wheelbase', spec.wheelbaseInches != null ? '${spec.wheelbaseInches!.toStringAsFixed(1)} in' : null), - ]), + ], colors, theme), _buildSpecGroup('FUEL', [ _SpecRow('Tank Capacity', spec.fuelCapacityGallons != null ? '${spec.fuelCapacityGallons!.toStringAsFixed(1)} gal' : null), _SpecRow('Fuel Economy', spec.fuelEconomyMpg != null ? '${spec.fuelEconomyMpg!.toStringAsFixed(0)} MPG' : null), _SpecRow('Estimated Range', spec.estimatedRangeMiles != null ? '~${spec.estimatedRangeMiles!.toStringAsFixed(0)} miles' : null), - ]), + ], colors, theme), _buildSpecGroup('CHASSIS & BRAKES', [ _SpecRow('Frame Type', spec.frameType), _SpecRow('Front Suspension', spec.frontSuspension), @@ -343,48 +537,84 @@ class MotorcycleDetailScreen extends ConsumerWidget { _SpecRow('ABS System', spec.absType), _SpecRow('Front Tire', spec.frontTireSize), _SpecRow('Rear Tire', spec.rearTireSize), - ]), + ], colors, theme), _buildSpecGroup('TRANSMISSION', [ _SpecRow('Transmission Type', spec.transmissionType), _SpecRow('Gears', spec.gearCount?.toString()), _SpecRow('Final Drive', spec.finalDrive), - ]), + ], colors, theme), if (spec.source != null) ...[ const SizedBox(height: 12), Text( 'Specifications Source: ${spec.source}', - style: const TextStyle(fontSize: 12, color: Colors.white38), + style: TextStyle( + fontSize: 11, + color: theme.colorScheme.onSurface.withValues(alpha: 0.4), + fontFamily: 'Inter', + ), ), ], ], ); } - Widget _buildSpecGroup(String title, List<_SpecRow> rows) { + Widget _buildSpecGroup( + String title, + List<_SpecRow> rows, + MotorSignalThemeColors colors, + ThemeData theme, + ) { final validRows = rows.where((r) => r.value != null && r.value!.isNotEmpty).toList(); - if (validRows.isEmpty) return const SizedBox(); + if (validRows.isEmpty) return const SizedBox.shrink(); return Card( + elevation: 1, + color: colors.surfaceContainer, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: colors.outlineVariant), + ), margin: const EdgeInsets.only(bottom: 12), child: Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(14.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.cyanAccent, fontSize: 14, letterSpacing: 1.0)), + Text( + title, + style: TextStyle( + fontFamily: 'Sora', + fontWeight: FontWeight.bold, + color: colors.cyberCyan, + fontSize: 12, + letterSpacing: 0.8, + ), + ), const SizedBox(height: 8), ...validRows.map( (r) => Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(r.label, style: const TextStyle(color: Colors.white70)), - Flexible( + Text( + r.label, + style: TextStyle( + fontSize: 12, + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + const SizedBox(width: 8), + Expanded( child: Text( r.value!, textAlign: TextAlign.right, - style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white), + style: TextStyle( + fontWeight: FontWeight.bold, + fontFamily: 'JetBrains Mono', + fontSize: 12, + color: theme.colorScheme.onSurface, + ), ), ), ], @@ -412,7 +642,10 @@ class MotorcycleDetailScreen extends ConsumerWidget { TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), ElevatedButton( onPressed: () { - ref.read(garageControllerProvider).updateNickname(bike.id, controller.text.trim().isNotEmpty ? controller.text.trim() : null); + ref.read(garageControllerProvider).updateNickname( + bike.id, + controller.text.trim().isNotEmpty ? controller.text.trim() : null, + ); Navigator.pop(context); }, child: const Text('Save'), @@ -423,17 +656,24 @@ class MotorcycleDetailScreen extends ConsumerWidget { ); } - void _confirmDelete(BuildContext context, WidgetRef ref, GarageMotorcycle bike) { + void _confirmDelete( + BuildContext context, + WidgetRef ref, + GarageMotorcycle bike, + MotorSignalThemeColors colors, + ) { showDialog( context: context, builder: (context) { return AlertDialog( title: const Text('Delete Motorcycle?'), - content: Text('Are you sure you want to remove ${bike.makeName} ${bike.modelName} from your Garage? Historical rides associated with this motorcycle will be preserved.'), + content: Text( + 'Are you sure you want to remove ${bike.makeName} ${bike.modelName} from your Garage? Historical rides associated with this motorcycle will be preserved.', + ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), ElevatedButton( - style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent), + style: ElevatedButton.styleFrom(backgroundColor: colors.raceRed), onPressed: () { ref.read(garageControllerProvider).deleteMotorcycle(bike.id); Navigator.pop(context); @@ -453,3 +693,4 @@ class _SpecRow { final String? value; _SpecRow(this.label, this.value); } + diff --git a/lib/features/garage/widgets/evolution_motorcycle_detail_view.dart b/lib/features/garage/widgets/evolution_motorcycle_detail_view.dart deleted file mode 100644 index 8c21eec..0000000 --- a/lib/features/garage/widgets/evolution_motorcycle_detail_view.dart +++ /dev/null @@ -1,269 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../database/database.dart'; -import '../../../models/motorcycle_models.dart'; -import 'motorcycle_photo.dart'; - -class EvolutionMotorcycleDetailView extends ConsumerWidget { - final GarageMotorcycle bike; - final MotorcycleSpecification? spec; - final VoidCallback onEdit; - final VoidCallback onDelete; - - const EvolutionMotorcycleDetailView({ - super.key, - required this.bike, - required this.spec, - required this.onEdit, - required this.onDelete, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final title = '${bike.year} ${bike.makeName} ${bike.modelName}'.toUpperCase(); - - final powerHp = spec?.horsepower != null && spec!.horsepower! > 0 - ? spec!.horsepower!.toStringAsFixed(0) - : '124'; - final torqueVal = spec?.torqueLbFt; - final torqueLbFt = torqueVal != null && torqueVal > 0 - ? torqueVal.toStringAsFixed(1) - : '76.6'; - final wetWeight = spec?.wetWeightLbs != null && spec!.wetWeightLbs! > 0 - ? spec!.wetWeightLbs!.toStringAsFixed(0) - : (spec?.dryWeightLbs != null && spec!.dryWeightLbs! > 0 - ? (spec!.dryWeightLbs! + 25).toStringAsFixed(0) - : '432'); - final displacement = spec?.displacementCc != null && spec!.displacementCc! > 0 - ? '${spec!.displacementCc!.toStringAsFixed(0)} cc' - : '599 cc'; - final engineType = spec?.engineConfiguration ?? 'Inline-4'; - final cylinders = spec?.cylinderCount != null ? '${spec!.cylinderCount}' : '4'; - final redline = spec?.redlineRpm != null && spec!.redlineRpm! > 0 - ? '${spec!.redlineRpm} RPM' - : '15500 RPM'; - final compression = spec?.compressionRatio ?? '12.4:1'; - final fuelSystem = spec?.fuelSystem ?? 'Electronic Fuel Injection'; - final frameType = spec?.frameType ?? 'Aluminum Deltabox Perimeter'; - final frontBrake = spec?.frontBrake ?? 'Dual Hydraulic Discs'; - final absSystem = spec?.absType ?? 'Dual-Channel ABS'; - - return Scaffold( - backgroundColor: Colors.black, - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios_new, color: Color(0xFF00E5FF), size: 18), - onPressed: () => Navigator.of(context).pop(), - ), - centerTitle: true, - title: Text( - title, - style: const TextStyle( - fontFamily: 'Sora', - fontSize: 15, - fontWeight: FontWeight.w900, - color: Colors.white, - letterSpacing: 0.1, - ), - ), - actions: [ - IconButton( - icon: const Icon(Icons.edit_note, color: Colors.white70), - tooltip: 'Edit Bike', - onPressed: onEdit, - ), - IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.redAccent), - tooltip: 'Delete Bike', - onPressed: onDelete, - ), - ], - ), - body: LayoutBuilder( - builder: (context, constraints) { - final isLandscape = constraints.maxWidth > constraints.maxHeight; - if (isLandscape) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 4, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: _buildHeroSpecCard(bike, powerHp, torqueLbFt, wetWeight), - ), - ), - Expanded( - flex: 6, - child: SingleChildScrollView( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildEngineCard(displacement, engineType, cylinders, redline, compression, fuelSystem), - const SizedBox(height: 12), - _buildChassisCard(frameType, frontBrake, absSystem), - const SizedBox(height: 12), - _buildDimensionsCard(spec), - ], - ), - ), - ), - ], - ); - } - - return SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeroSpecCard(bike, powerHp, torqueLbFt, wetWeight), - const SizedBox(height: 16), - _buildEngineCard(displacement, engineType, cylinders, redline, compression, fuelSystem), - const SizedBox(height: 14), - _buildChassisCard(frameType, frontBrake, absSystem), - const SizedBox(height: 14), - _buildDimensionsCard(spec), - const SizedBox(height: 24), - ], - ), - ); - }, - ), - ); - } - - Widget _buildHeroSpecCard(GarageMotorcycle bike, String powerHp, String torqueLbFt, String wetWeight) { - return Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFF00E5FF).withValues(alpha: 0.6), width: 1.2), - ), - child: Column( - children: [ - MotorcyclePhoto( - year: bike.year, - make: bike.makeName, - model: bike.modelName, - height: 140, - borderRadius: BorderRadius.circular(12), - showAttribution: true, - fit: BoxFit.cover, - ), - const SizedBox(height: 14), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _buildBigKpi(powerHp, 'HP'), - Container(width: 1, height: 44, color: const Color(0xFF222222)), - _buildBigKpi(torqueLbFt, 'lb-ft'), - Container(width: 1, height: 44, color: const Color(0xFF222222)), - _buildBigKpi(wetWeight, 'lbs (Wet)'), - ], - ), - ], - ), - ); - } - - Widget _buildBigKpi(String value, String unit) { - return Expanded( - child: Column( - children: [ - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - value, - style: const TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 26, - fontWeight: FontWeight.w900, - color: Colors.white, - ), - ), - ), - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - unit, - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 10, - fontWeight: FontWeight.w700, - color: Color(0xFF00E5FF), - ), - ), - ), - ], - ), - ); - } - - Widget _buildEngineCard(String displacement, String engineType, String cylinders, String redline, String compression, String fuelSystem) { - return _buildCard('ENGINE', [ - _buildSpecLine('Displacement', displacement), - _buildSpecLine('Configuration', engineType), - _buildSpecLine('Cylinders', cylinders), - _buildSpecLine('Redline', redline), - _buildSpecLine('Compression Ratio', compression), - _buildSpecLine('Fuel System', fuelSystem), - ]); - } - - Widget _buildChassisCard(String frameType, String frontBrake, String absSystem) { - return _buildCard('CHASSIS & BRAKES', [ - _buildSpecLine('Frame Type', frameType), - _buildSpecLine('Front Brake', frontBrake), - _buildSpecLine('ABS System', absSystem), - ]); - } - - Widget _buildDimensionsCard(MotorcycleSpecification? spec) { - return _buildCard('WEIGHT & DIMENSIONS', [ - _buildSpecLine('Dry Weight', spec?.dryWeightLbs != null ? '${spec!.dryWeightLbs!.toStringAsFixed(0)} lbs' : '375 lbs'), - _buildSpecLine('Fuel Capacity', spec?.fuelCapacityGallons != null ? '${spec!.fuelCapacityGallons!.toStringAsFixed(1)} Gal' : '4.5 Gal'), - _buildSpecLine('Top Speed Potential', spec?.claimedTopSpeedMph != null ? '${spec!.claimedTopSpeedMph!.toStringAsFixed(0)} MPH' : '160+ MPH'), - ]); - } - - Widget _buildCard(String title, List children) { - return Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(14), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, style: const TextStyle(fontFamily: 'Sora', fontSize: 12, fontWeight: FontWeight.w800, color: Color(0xFF00E5FF), letterSpacing: 0.1)), - const SizedBox(height: 10), - ...children, - ], - ), - ); - } - - Widget _buildSpecLine(String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(label, style: const TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w600, color: Colors.white54)), - Flexible( - child: Text(value, textAlign: TextAlign.right, style: const TextStyle(fontFamily: 'JetBrains Mono', fontSize: 11, fontWeight: FontWeight.w700, color: Colors.white)), - ), - ], - ), - ); - } -} diff --git a/lib/features/history/history_screen.dart b/lib/features/history/history_screen.dart index f119ab9..1f47906 100644 --- a/lib/features/history/history_screen.dart +++ b/lib/features/history/history_screen.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'dart:math' as math; +import 'package:intl/intl.dart'; import '../../database/database.dart'; +import '../../core/responsive/responsive_layout.dart'; +import '../../core/theme/theme_extensions.dart'; import '../garage/garage_providers.dart'; import 'ride_detail_screen.dart'; @@ -13,20 +16,25 @@ class HistoryScreen extends ConsumerStatefulWidget { } class _HistoryScreenState extends ConsumerState { - int? - _selectedMotorcycleId; // null = All Motorcycles, -1 = Current, else specific ID + int? _selectedMotorcycleId; // null = All Motorcycles, -1 = Current, else specific ID @override Widget build(BuildContext context) { final database = ref.watch(databaseProvider); final garageBikesAsync = ref.watch(garageMotorcyclesStreamProvider); final defaultBike = ref.watch(defaultMotorcycleProvider); + final colors = context.motorColors; return Scaffold( - appBar: AppBar(title: const Text('Ride History')), + appBar: AppBar( + title: const Text( + 'Ride History', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), body: Column( children: [ - _buildFilterBar(garageBikesAsync.value ?? [], defaultBike), + _buildFilterBar(context, garageBikesAsync.value ?? [], defaultBike), Expanded( child: StreamBuilder>( stream: database.select(database.rides).watch(), @@ -57,148 +65,54 @@ class _HistoryScreenState extends ConsumerState { rides.sort((a, b) => b.startTime.compareTo(a.startTime)); if (rides.isEmpty) { - return const Center( - child: Text( - 'No recorded rides matching filter.', - style: TextStyle(fontSize: 18, color: Colors.grey), + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.history, + size: 64, + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3), + ), + const SizedBox(height: 16), + Text( + 'No recorded rides matching filter.', + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + ], ), ); } - return ListView.builder( - padding: const EdgeInsets.all(8), - itemCount: rides.length, - itemBuilder: (context, index) { - final ride = rides[index]; - final bikeName = - (ride.motorcycleYear != null && - ride.motorcycleMake != null && - ride.motorcycleModel != null) - ? '${ride.motorcycleYear} ${ride.motorcycleMake} ${ride.motorcycleModel}' - : null; + return ResponsiveLayout( + builder: (context, responsive) { + final isMultiColumn = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape); - return Card( - elevation: 4, - margin: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 4, - ), - child: InkWell( - key: ValueKey('history_ride_card_${ride.id}'), - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => RideDetailScreen(ride: ride), - ), - ); - }, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Row( - children: [ - Text( - 'Ride ${ride.id}', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - if (bikeName != null) ...[ - const SizedBox(width: 8), - Flexible( - child: Container( - padding: - const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.cyanAccent - .withValues(alpha: 0.15), - borderRadius: - BorderRadius.circular(4), - border: Border.all( - color: Colors.cyanAccent - .withValues(alpha: 0.5), - ), - ), - child: Text( - bikeName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 11, - fontWeight: FontWeight.bold, - color: Colors.cyanAccent, - ), - ), - ), - ), - ], - ], - ), - ), - const SizedBox(width: 8), - Text( - ride.startTime.toLocal().toString().split( - '.', - )[0], - style: const TextStyle( - color: Colors.grey, - fontSize: 11, - ), - ), - ], - ), - const Divider(), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - _buildStatCol( - 'TRIP', - '${ride.distanceMiles.toStringAsFixed(1)} mi', - ), - _buildStatCol( - 'TIME', - '${(ride.movingTime / 60).toStringAsFixed(0)} min', - ), - _buildStatCol( - 'TOP SPD', - '${ride.topSpeedMph.toStringAsFixed(0)} mph', - ), - ], - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - _buildStatCol( - 'MAX LEAN', - '${math.max(ride.maxLeanLeft, ride.maxLeanRight).toStringAsFixed(0)}°', - ), - _buildStatCol( - 'HARD BRK', - '${ride.maxBrakingG.toStringAsFixed(2)}G', - ), - _buildStatCol( - 'ELEV GAIN', - '${ride.elevationGain.toStringAsFixed(0)}m', - ), - ], - ), - ], - ), + if (isMultiColumn) { + return GridView.builder( + padding: const EdgeInsets.all(12), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 600, + mainAxisExtent: 175, + crossAxisSpacing: 12, + mainAxisSpacing: 12, ), - ), + itemCount: rides.length, + itemBuilder: (context, index) { + return _buildRideCard(context, rides[index], colors); + }, + ); + } + + return ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + itemCount: rides.length, + itemBuilder: (context, index) { + return _buildRideCard(context, rides[index], colors); + }, ); }, ); @@ -210,45 +124,186 @@ class _HistoryScreenState extends ConsumerState { ); } + Widget _buildRideCard(BuildContext context, Ride ride, MotorSignalThemeColors colors) { + final bikeName = (ride.motorcycleYear != null && + ride.motorcycleMake != null && + ride.motorcycleModel != null) + ? '${ride.motorcycleYear} ${ride.motorcycleMake} ${ride.motorcycleModel}' + : null; + + final theme = Theme.of(context); + final dateFormatted = DateFormat('yyyy-MM-dd HH:mm').format(ride.startTime.toLocal()); + + return Card( + elevation: 2, + margin: const EdgeInsets.symmetric(vertical: 6), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: colors.outlineVariant), + ), + color: colors.surfaceContainer, + child: InkWell( + key: ValueKey('history_ride_card_${ride.id}'), + borderRadius: BorderRadius.circular(12), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => RideDetailScreen(ride: ride), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.all(14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Ride ${ride.id}', + style: TextStyle( + fontWeight: FontWeight.w900, + fontSize: 16, + fontFamily: 'Sora', + color: theme.colorScheme.onSurface, + ), + ), + if (bikeName != null) ...[ + const SizedBox(width: 6), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 2, + ), + decoration: BoxDecoration( + color: colors.cyberCyan.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: colors.cyberCyan.withValues(alpha: 0.4), + ), + ), + child: Text( + bikeName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.5, + fontWeight: FontWeight.bold, + color: colors.cyberCyan, + ), + ), + ), + ), + ] else ...[ + const Spacer(), + ], + const SizedBox(width: 6), + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerRight, + child: Text( + dateFormatted, + style: TextStyle( + color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + fontSize: 10.5, + fontFamily: 'JetBrains Mono', + ), + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Divider(height: 1, color: colors.outlineVariant), + const SizedBox(height: 10), + Row( + children: [ + Expanded(child: _buildStatCol(context, 'TRIP', '${ride.distanceMiles.toStringAsFixed(1)} mi')), + Expanded(child: _buildStatCol(context, 'TIME', '${(ride.movingTime / 60).toStringAsFixed(0)} min')), + Expanded(child: _buildStatCol(context, 'TOP SPD', '${ride.topSpeedMph.toStringAsFixed(0)} mph')), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: _buildStatCol( + context, + 'MAX LEAN', + '${math.max(ride.maxLeanLeft, ride.maxLeanRight).toStringAsFixed(0)}°', + valueColor: colors.cyberCyan, + ), + ), + Expanded( + child: _buildStatCol( + context, + 'HARD BRK', + '${ride.maxBrakingG.toStringAsFixed(2)}G', + valueColor: colors.gForceNegative, + ), + ), + Expanded( + child: _buildStatCol( + context, + 'ELEV GAIN', + '${ride.elevationGain.toStringAsFixed(0)}m', + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + Widget _buildFilterBar( + BuildContext context, List bikes, GarageMotorcycle? defaultBike, ) { - if (bikes.isEmpty) return const SizedBox(); + if (bikes.isEmpty) return const SizedBox.shrink(); + + final colors = context.motorColors; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Row( children: [ - const Icon(Icons.filter_alt, color: Colors.cyanAccent, size: 20), + Icon(Icons.filter_alt, color: colors.cyberCyan, size: 20), const SizedBox(width: 8), Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( - color: Colors.white10, + color: colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(8), + border: Border.all(color: colors.outlineVariant), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: _selectedMotorcycleId, isExpanded: true, - dropdownColor: const Color(0xFF1E1E1E), + dropdownColor: colors.surfaceContainerHigh, hint: const Text('Filter by Motorcycle'), items: [ const DropdownMenuItem( value: null, - child: Text('All Motorcycles'), + child: Text('All Motorcycles', overflow: TextOverflow.ellipsis), ), if (defaultBike != null) DropdownMenuItem( value: -1, - child: Text('Current (${defaultBike.modelName})'), + child: Text('Current (${defaultBike.modelName})', overflow: TextOverflow.ellipsis), ), ...bikes.map( (b) => DropdownMenuItem( value: b.id, - child: Text('${b.year} ${b.makeName} ${b.modelName}'), + child: Text('${b.year} ${b.makeName} ${b.modelName}', overflow: TextOverflow.ellipsis), ), ), ], @@ -266,16 +321,38 @@ class _HistoryScreenState extends ConsumerState { ); } - Widget _buildStatCol(String label, String val) { + Widget _buildStatCol(BuildContext context, String label, String val, {Color? valueColor}) { + final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)), Text( - val, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 9.5, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, + color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + ), + ), + const SizedBox(height: 2), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + val, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + fontFamily: 'JetBrains Mono', + color: valueColor ?? theme.colorScheme.onSurface, + ), + ), ), ], ); } } + + diff --git a/lib/features/history/ride_detail_screen.dart b/lib/features/history/ride_detail_screen.dart index 7876458..2930350 100644 --- a/lib/features/history/ride_detail_screen.dart +++ b/lib/features/history/ride_detail_screen.dart @@ -1,15 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:go_router/go_router.dart'; import '../../database/database.dart'; +import '../../core/responsive/responsive_layout.dart'; +import '../../core/theme/theme_extensions.dart'; import '../../services/export_service.dart'; import '../../services/entitlements/entitlement_providers.dart'; import '../../services/entitlements/feature_access_service.dart'; +import '../../services/github/github_sync_service.dart'; import '../entitlements/widgets/pro_lock_badge.dart'; import '../replay/replay_screen.dart'; import '../map/map_screen.dart'; -import 'package:go_router/go_router.dart'; -import '../../core/theme/evolution_theme_config.dart'; -import 'widgets/evolution_ride_details_view.dart'; class RideDetailScreen extends ConsumerStatefulWidget { final Ride ride; @@ -45,304 +47,586 @@ class _RideDetailScreenState extends ConsumerState { @override Widget build(BuildContext context) { - final themeMode = ref.watch(dashboardThemeModeProvider); - if (themeMode == DashboardThemeMode.evolution) { - return EvolutionRideDetailsView(ride: widget.ride, events: _events); - } + final ride = widget.ride; + final colors = context.motorColors; + final theme = Theme.of(context); + + final bikeName = (ride.motorcycleYear != null && + ride.motorcycleMake != null && + ride.motorcycleModel != null) + ? '${ride.motorcycleYear} ${ride.motorcycleMake} ${ride.motorcycleModel}' + .toUpperCase() + : null; + + final durationSec = ride.endTime != null + ? ride.endTime!.difference(ride.startTime).inSeconds + : (ride.movingTime + ride.stoppedTime); + final durationMin = (durationSec / 60).toStringAsFixed(0); + final movingMin = (ride.movingTime / 60).toStringAsFixed(0); + + final syncService = ref.watch(githubSyncServiceProvider); + final isUploaded = syncService.isRideUploaded(ride.id); + final commitSha = syncService.getRideCommitSha(ride.id); - final r = widget.ride; return Scaffold( - appBar: AppBar(title: Text('Ride ${r.id} Details')), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - if (r.motorcycleMake != null && r.motorcycleModel != null) ...[ - Container( + appBar: AppBar( + title: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + 'Ride #${ride.id} Telemetry', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + actions: [ + IconButton( + icon: Icon(Icons.share, color: colors.cyberCyan), + tooltip: 'Export CSV', + onPressed: () async { + final isPro = ref + .read(featureAccessProvider) + .canAccess(MotorSignalFeature.csvExport); + if (!isPro) { + showProUpgradeSheet( + context, + ref, + feature: MotorSignalFeature.csvExport, + ); + return; + } + final exporter = ref.read(exportServiceProvider); + await exporter.exportRide(ride.id, asGpx: false); + }, + ), + IconButton( + icon: Icon(Icons.delete_outline, color: colors.raceRed), + tooltip: 'Delete Ride', + onPressed: () => _confirmDelete(context, ride), + ), + ], + ), + body: ResponsiveLayout( + builder: (context, responsive) { + final isWide = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape); + + if (isWide) { + return SingleChildScrollView( padding: const EdgeInsets.all(16), - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.cyanAccent.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Colors.cyanAccent.withValues(alpha: 0.5), - ), - ), - child: Row( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Icon( - Icons.two_wheeler, - color: Colors.cyanAccent, - size: 32, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${r.motorcycleYear ?? ""} ${r.motorcycleMake} ${r.motorcycleModel}', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - softWrap: true, + _buildGitHubSyncBanner(context, ref, syncService, isUploaded, commitSha, colors), + const SizedBox(height: 16), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 5, + child: Column( + children: [ + _buildHeroHeaderRow(context, ride, bikeName, colors), + const SizedBox(height: 16), + _buildSessionOverview(ride, durationMin, movingMin, colors, theme), + const SizedBox(height: 16), + _buildActionButtons(context, ride, colors, isNarrow: responsive.width < 800), + ], ), - if (r.motorcycleVariant != null && - r.motorcycleVariant!.isNotEmpty) - Text( - r.motorcycleVariant!, - style: const TextStyle( - color: Colors.white70, - fontSize: 12, + ), + const SizedBox(width: 16), + Expanded( + flex: 5, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHandlingSection( + ride, + colors, + isLandscape: responsive.isExpanded && responsive.width >= 900, ), - softWrap: true, - ), - if (r.motorcycleId != null) ...[ - const SizedBox(height: 8), - Align( - alignment: Alignment.centerLeft, - child: OutlinedButton( - style: OutlinedButton.styleFrom( - foregroundColor: Colors.cyanAccent, - side: const BorderSide( - color: Colors.cyanAccent, - ), - ), - onPressed: () => - context.push('/garage/${r.motorcycleId}'), - child: const Text('View Specs'), - ), - ), - ], - ], - ), + const SizedBox(height: 16), + _buildForensicEventsSection(context, colors, theme), + ], + ), + ), + ], ), ], ), + ); + } + + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildGitHubSyncBanner(context, ref, syncService, isUploaded, commitSha, colors), + const SizedBox(height: 14), + _buildHeroHeaderRow(context, ride, bikeName, colors), + const SizedBox(height: 16), + _buildSessionOverview(ride, durationMin, movingMin, colors, theme), + const SizedBox(height: 16), + _buildHandlingSection(ride, colors, isLandscape: false), + const SizedBox(height: 16), + _buildForensicEventsSection(context, colors, theme), + const SizedBox(height: 20), + _buildActionButtons(context, ride, colors, isNarrow: responsive.width < 450), + const SizedBox(height: 24), + ], + ), + ); + }, + ), + ); + } + + Widget _buildGitHubSyncBanner( + BuildContext context, + WidgetRef ref, + GitHubSyncService syncService, + bool isUploaded, + String? commitSha, + MotorSignalThemeColors colors, + ) { + final isSyncing = syncService.state == GitHubUploadState.uploading; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: colors.surfaceContainer, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isUploaded + ? colors.launchGreen.withValues(alpha: 0.6) + : colors.cyberCyan.withValues(alpha: 0.4), + width: 1.2, + ), + ), + child: Row( + children: [ + Icon( + isUploaded + ? Icons.cloud_done + : (isSyncing ? Icons.cloud_sync : Icons.cloud_upload_outlined), + color: isUploaded ? colors.launchGreen : colors.cyberCyan, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + isUploaded + ? 'GITHUB LOGS SYNCHRONIZED' + : (isSyncing ? 'UPLOADING TO GITHUB...' : 'GITHUB LOG SYNC'), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: 'Sora', + fontSize: 10.5, + fontWeight: FontWeight.w800, + color: isUploaded ? colors.launchGreen : Theme.of(context).colorScheme.onSurface, + ), + ), + Text( + isUploaded + ? 'Commit: ${commitSha != null && commitSha.length >= 7 ? commitSha.substring(0, 7) : "synced"}' + : 'Repository: bponders12-cell/ride_logs', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 9, + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + ], + ), + ), + if (!isUploaded) ...[ + const SizedBox(width: 6), + TextButton( + onPressed: isSyncing + ? null + : () async { + final result = await syncService.uploadRide( + widget.ride.id, + isManual: true, + ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(result.message), + backgroundColor: result.success + ? colors.launchGreen + : colors.raceRed, + ), + ); + } + }, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + backgroundColor: colors.cyberCyan.withValues(alpha: 0.15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + isSyncing ? 'SYNCING...' : 'UPLOAD', + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 9.5, + fontWeight: FontWeight.w800, + color: colors.cyberCyan, + ), + ), ), ], + ], + ), + ); + } - // Map Preview Card - Card( - clipBehavior: Clip.antiAlias, - margin: const EdgeInsets.only(bottom: 16), - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.black45, - border: Border.all(color: Colors.white12), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon(Icons.map, color: Colors.cyanAccent, size: 36), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'ROUTE PREVIEW', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - letterSpacing: 1.5, - color: Colors.cyanAccent, - ), - softWrap: true, - ), - const SizedBox(height: 4), - const Text( - 'View ride path and telemetry events on interactive map', - style: TextStyle(fontSize: 12, color: Colors.white70), - softWrap: true, - ), - const SizedBox(height: 12), - Align( - alignment: Alignment.centerLeft, - child: ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => MapScreen(rideId: r.id), - ), - ); - }, - child: const Text('VIEW ROUTE'), - ), - ), - ], + Widget _buildHeroHeaderRow( + BuildContext context, + Ride ride, + String? bikeName, + MotorSignalThemeColors colors, + ) { + return Row( + children: [ + Expanded( + child: Container( + constraints: const BoxConstraints(minHeight: 120), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), + decoration: BoxDecoration( + color: colors.surfaceContainer, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: colors.outlineVariant, width: 1.2), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.two_wheeler, + color: colors.cyberCyan, + size: 32, + ), + const SizedBox(height: 4), + Text( + bikeName ?? 'MOTORCYCLE', + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: 'Sora', + fontSize: 10.5, + fontWeight: FontWeight.w800, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + if (ride.motorcycleId != null) ...[ + const SizedBox(height: 4), + InkWell( + onTap: () => context.push('/garage/${ride.motorcycleId}'), + child: Text( + 'VIEW SPECS >', + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 8.5, + fontWeight: FontWeight.bold, + color: colors.cyberCyan, + ), ), ), ], - ), + ], ), ), - - _buildSectionHeader('SUMMARY'), - _buildStatRow('Distance', '${r.distanceMiles.toStringAsFixed(2)} mi'), - _buildStatRow( - 'Duration', - '${r.endTime?.difference(r.startTime).inMinutes ?? 0} min', - ), - _buildStatRow( - 'Moving Time', - '${(r.movingTime / 60).toStringAsFixed(1)} min', - ), - _buildStatRow( - 'Avg Speed', - '${r.averageSpeed.toStringAsFixed(1)} mph', - ), - _buildStatRow('Top Speed', '${r.topSpeedMph.toStringAsFixed(1)} mph'), - const Divider(), - - _buildSectionHeader('HANDLING'), - _buildStatRow( - 'Max Lean (Left)', - '${r.maxLeanLeft.toStringAsFixed(1)}°', - ), - _buildStatRow( - 'Max Lean (Right)', - '${r.maxLeanRight.toStringAsFixed(1)}°', - ), - _buildStatRow( - 'Max Acceleration', - '${r.maxAccelG.toStringAsFixed(2)} G', - ), - _buildStatRow('Max Braking', '${r.maxBrakingG.toStringAsFixed(2)} G'), - const Divider(), - - _buildSectionHeader('ELEVATION'), - _buildStatRow('Gain', '${r.elevationGain.toStringAsFixed(0)} m'), - _buildStatRow('Loss', '${r.elevationLoss.toStringAsFixed(0)} m'), - const Divider(), - - _buildSectionHeader('EVENTS'), - if (_isLoadingEvents) - const Center(child: CircularProgressIndicator()) - else if (_events.isEmpty) - const Padding( - padding: EdgeInsets.all(8.0), - child: Text('No events recorded.'), - ) - else - ..._events.map( - (e) => ListTile( - dense: true, - leading: const Icon( - Icons.warning_amber, - color: Colors.amberAccent, - ), - title: Text(e.eventType), - subtitle: Text(e.timestamp.toLocal().toString().split('.')[0]), - trailing: Text( - e.value.toStringAsFixed(1), - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - const Divider(), - - _buildSectionHeader('ACTIONS'), - ElevatedButton.icon( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => ReplayScreen(rideId: r.id)), - ); - }, - icon: const Icon(Icons.play_arrow), - label: const Text('Replay Ride'), - ), - const SizedBox(height: 8), - OutlinedButton.icon( - onPressed: () { - ref.read(exportServiceProvider).exportRide(r.id, asGpx: true); - }, - icon: const Icon(Icons.share), - label: const Text('Export GPX'), - ), - const SizedBox(height: 8), - Builder( - builder: (context) { - final isProCsv = ref - .watch(featureAccessProvider) - .canAccess(MotorSignalFeature.csvExport); - return OutlinedButton.icon( - onPressed: () { - if (!isProCsv) { - showProUpgradeSheet( - context, - ref, - feature: MotorSignalFeature.csvExport, - ); - return; - } - ref - .read(exportServiceProvider) - .exportRide(r.id, asGpx: false); - }, - icon: const Icon(Icons.table_chart), - label: FittedBox( - fit: BoxFit.scaleDown, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Export CSV'), - if (!isProCsv) ...[ - const SizedBox(width: 8), - const ProBadge(), - ], - ], + ), + const SizedBox(width: 10), + Expanded( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => MapScreen(rideId: ride.id), + ), + ); + }, + borderRadius: BorderRadius.circular(14), + child: Container( + constraints: const BoxConstraints(minHeight: 120), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), + decoration: BoxDecoration( + color: colors.surfaceContainer, + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: colors.cyberCyan.withValues(alpha: 0.5), + width: 1.2, ), ), - ); - }, - ), - const SizedBox(height: 8), - TextButton.icon( - onPressed: () async { - final confirm = await showDialog( - context: context, - builder: (c) => AlertDialog( - title: const Text('Delete Ride?'), - content: const Text('This cannot be undone.'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(c, false), - child: const Text('CANCEL'), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.map_rounded, + color: colors.cyberCyan, + size: 32, ), - TextButton( - onPressed: () => Navigator.pop(c, true), - child: const Text( - 'DELETE', - style: TextStyle(color: Colors.redAccent), + const SizedBox(height: 4), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + 'ROUTE PREVIEW', + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 10.5, + fontWeight: FontWeight.w900, + color: colors.cyberCyan, + letterSpacing: 0.08, + ), + ), + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: colors.cyberCyan.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(6), + ), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + 'VIEW MAP >', + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 8, + fontWeight: FontWeight.w800, + color: colors.cyberCyan, + ), + ), ), ), ], ), - ); + ), + ), + ), + ), + ], + ); + } - if (confirm == true && mounted) { - final db = ref.read(databaseProvider); - await (db.delete( - db.telemetrySamples, - )..where((t) => t.rideId.equals(r.id))).go(); - await (db.delete( - db.rideEvents, - )..where((e) => e.rideId.equals(r.id))).go(); - await (db.delete( - db.rides, - )..where((ride) => ride.id.equals(r.id))).go(); - if (context.mounted) { - Navigator.of(context).pop(); - } - } - }, - icon: const Icon(Icons.delete, color: Colors.redAccent), - label: const Text( - 'Delete Ride', - style: TextStyle(color: Colors.redAccent), + Widget _buildSessionOverview( + Ride ride, + String durationMin, + String movingMin, + MotorSignalThemeColors colors, + ThemeData theme, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader('SESSION OVERVIEW', colors), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colors.surfaceContainer, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: colors.outlineVariant, width: 1.2), + ), + child: Column( + children: [ + _buildMetricRow( + 'DISTANCE:', + '${ride.distanceMiles.toStringAsFixed(2)} mi', + 'DURATION:', + '$durationMin min', + theme, + colors, + ), + Divider(color: colors.outlineVariant, height: 18), + _buildMetricRow( + 'MOVING TIME:', + '$movingMin min', + 'AVG SPEED:', + '${ride.averageSpeed.toStringAsFixed(1)} mph', + theme, + colors, + ), + Divider(color: colors.outlineVariant, height: 18), + _buildMetricRow( + 'TOP SPEED:', + '${ride.topSpeedMph.toStringAsFixed(1)} mph', + 'DATE:', + DateFormat('MM/dd/yyyy').format(ride.startTime.toLocal()), + theme, + colors, + ), + Divider(color: colors.outlineVariant, height: 18), + _buildMetricRow( + 'ELEV GAIN:', + '${ride.elevationGain.toStringAsFixed(0)} m', + 'ELEV LOSS:', + '${ride.elevationLoss.toStringAsFixed(0)} m', + theme, + colors, + ), + ], + ), + ), + ], + ); + } + + Widget _buildHandlingSection( + Ride ride, + MotorSignalThemeColors colors, { + required bool isLandscape, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader('HANDLING & DYNAMICS', colors), + const SizedBox(height: 8), + _buildResponsiveHandlingCards(ride, colors, isLandscape), + ], + ); + } + + Widget _buildResponsiveHandlingCards( + Ride ride, + MotorSignalThemeColors colors, + bool isLandscape, + ) { + final cards = [ + _buildHandlingKpiCard( + 'MAX LEAN (LEFT)', + '${ride.maxLeanLeft.toStringAsFixed(1)}°', + Icons.turn_left, + colors.leanAngleLeft, + colors, + ), + _buildHandlingKpiCard( + 'MAX LEAN (RIGHT)', + '${ride.maxLeanRight.toStringAsFixed(1)}°', + Icons.turn_right, + colors.leanAngleRight, + colors, + ), + _buildHandlingKpiCard( + 'MAX ACCELERATION', + '${ride.maxAccelG.toStringAsFixed(2)} G', + Icons.speed, + colors.gForcePositive, + colors, + ), + _buildHandlingKpiCard( + 'MAX BRAKING', + '${ride.maxBrakingG.toStringAsFixed(2)} G', + Icons.pan_tool_alt, + colors.gForceNegative, + colors, + ), + ]; + + if (isLandscape) { + return Row( + children: cards + .map( + (c) => Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: c, + ), + ), + ) + .toList(), + ); + } + + return Column( + children: [ + Row( + children: [ + Expanded(child: cards[0]), + const SizedBox(width: 8), + Expanded(child: cards[1]), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded(child: cards[2]), + const SizedBox(width: 8), + Expanded(child: cards[3]), + ], + ), + ], + ); + } + + Widget _buildHandlingKpiCard( + String label, + String value, + IconData icon, + Color accentColor, + MotorSignalThemeColors colors, + ) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decoration: BoxDecoration( + color: colors.surfaceContainer, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: colors.outlineVariant, width: 1.2), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: 'Inter', + fontSize: 9.5, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + ), + Icon(icon, color: accentColor, size: 16), + ], + ), + const SizedBox(height: 6), + FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + value, + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 18, + fontWeight: FontWeight.w900, + color: Theme.of(context).colorScheme.onSurface, + ), ), ), ], @@ -350,42 +634,457 @@ class _RideDetailScreenState extends ConsumerState { ); } - Widget _buildSectionHeader(String title) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text( - title, - style: const TextStyle( - color: Colors.cyanAccent, - fontSize: 14, - fontWeight: FontWeight.bold, - letterSpacing: 2, + Widget _buildForensicEventsSection( + BuildContext context, + MotorSignalThemeColors colors, + ThemeData theme, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: _buildSectionHeader('FORENSIC EVENTS (${_events.length})', colors), + ), + if (_events.isNotEmpty) + Text( + 'TAP TO INSPECT', + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 9, + fontWeight: FontWeight.w700, + color: colors.cyberCyan, + ), + ), + ], + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: colors.surfaceContainer, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: colors.outlineVariant, width: 1.2), + ), + child: _isLoadingEvents + ? const Padding( + padding: EdgeInsets.all(16), + child: Center(child: CircularProgressIndicator()), + ) + : _events.isEmpty + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Center( + child: Text( + 'No threshold handling events registered during this session.', + style: TextStyle( + fontFamily: 'Inter', + fontSize: 12, + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + ), + ) + : Column( + children: _events + .take(15) + .map( + (event) => _buildInteractiveEventTile(context, event, colors), + ) + .toList(), + ), + ), + ], + ); + } + + Widget _buildInteractiveEventTile( + BuildContext context, + RideEvent event, + MotorSignalThemeColors colors, + ) { + Color badgeColor = colors.warningAmber; + IconData eventIcon = Icons.warning_amber_rounded; + + if (event.eventType.toLowerCase().contains('accel')) { + badgeColor = colors.launchGreen; + eventIcon = Icons.arrow_upward; + } else if (event.eventType.toLowerCase().contains('brak')) { + badgeColor = colors.raceRed; + eventIcon = Icons.arrow_downward; + } else if (event.eventType.toLowerCase().contains('corner') || + event.eventType.toLowerCase().contains('lean')) { + badgeColor = colors.cyberCyan; + eventIcon = Icons.cached; + } + + String formattedValue = '${event.value.toStringAsFixed(1)}°'; + if (event.eventType.toLowerCase().contains('accel') || + event.eventType.toLowerCase().contains('brak')) { + formattedValue = '${event.value.toStringAsFixed(2)} G'; + } + + final timeStr = DateFormat('HH:mm:ss.SSS').format(event.timestamp.toLocal()); + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _showEventDetailsDialog( + context, + event, + formattedValue, + timeStr, + badgeColor, + ), + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 6), + child: Row( + children: [ + Icon(eventIcon, color: badgeColor, size: 16), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + event.eventType.toUpperCase(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 11, + fontWeight: FontWeight.w800, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + Text( + timeStr, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 9, + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5), + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: badgeColor.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + formattedValue, + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 11, + fontWeight: FontWeight.w700, + color: badgeColor, + ), + ), + ), + const SizedBox(width: 6), + Icon( + Icons.chevron_right, + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3), + size: 16, + ), + ], + ), ), ), ); } - Widget _buildStatRow(String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text(label, style: const TextStyle(color: Colors.white70)), - ), - const SizedBox(width: 12), - Flexible( - child: Text( - value, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - textAlign: TextAlign.right, - softWrap: true, - ), + void _showEventDetailsDialog( + BuildContext context, + RideEvent event, + String formattedValue, + String timeStr, + Color badgeColor, + ) { + showDialog( + context: context, + builder: (c) => AlertDialog( + title: Row( + children: [ + Icon(Icons.info_outline, color: badgeColor), + const SizedBox(width: 8), + Text(event.eventType.toUpperCase()), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Timestamp: $timeStr'), + const SizedBox(height: 8), + Text('Recorded Value: $formattedValue'), + const SizedBox(height: 8), + Text('Ride ID: ${event.rideId}'), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(c), + child: const Text('CLOSE'), ), ], ), ); } + + Widget _buildActionButtons( + BuildContext context, + Ride ride, + MotorSignalThemeColors colors, { + required bool isNarrow, + }) { + Widget buildReplayButton() => OutlinedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ReplayScreen(rideId: ride.id), + ), + ); + }, + style: OutlinedButton.styleFrom( + foregroundColor: colors.cyberCyan, + side: BorderSide(color: colors.cyberCyan, width: 1.5), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + child: const FittedBox( + fit: BoxFit.scaleDown, + child: Text( + 'REPLAY RIDE', + style: TextStyle( + fontFamily: 'Sora', + fontWeight: FontWeight.w800, + fontSize: 12, + ), + ), + ), + ); + + Widget buildGpxButton() => OutlinedButton( + onPressed: () async { + final exporter = ref.read(exportServiceProvider); + await exporter.exportRide(ride.id, asGpx: true); + }, + style: OutlinedButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.onSurface, + side: BorderSide(color: colors.outlineVariant, width: 1.2), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + child: const FittedBox( + fit: BoxFit.scaleDown, + child: Text( + 'EXPORT GPX', + style: TextStyle( + fontFamily: 'Sora', + fontWeight: FontWeight.w700, + fontSize: 12, + ), + ), + ), + ); + + Widget buildCsvButton() => Builder( + builder: (context) { + final isProCsv = ref + .watch(featureAccessProvider) + .canAccess(MotorSignalFeature.csvExport); + return OutlinedButton( + onPressed: () async { + if (!isProCsv) { + showProUpgradeSheet( + context, + ref, + feature: MotorSignalFeature.csvExport, + ); + return; + } + final exporter = ref.read(exportServiceProvider); + await exporter.exportRide(ride.id, asGpx: false); + }, + style: OutlinedButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.onSurface, + side: BorderSide(color: colors.outlineVariant, width: 1.2), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'EXPORT CSV', + style: TextStyle( + fontFamily: 'Sora', + fontWeight: FontWeight.w700, + fontSize: 12, + ), + ), + if (!isProCsv) ...[ + const SizedBox(width: 6), + const ProBadge( + fontSize: 8, + padding: EdgeInsets.symmetric(horizontal: 4, vertical: 1), + ), + ], + ], + ), + ), + ); + }, + ); + + if (isNarrow) { + return Column( + children: [ + Row( + children: [ + Expanded(child: buildReplayButton()), + const SizedBox(width: 8), + Expanded(child: buildGpxButton()), + ], + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: buildCsvButton(), + ), + ], + ); + } + + return Row( + children: [ + Expanded(child: buildReplayButton()), + const SizedBox(width: 8), + Expanded(child: buildGpxButton()), + const SizedBox(width: 8), + Expanded(child: buildCsvButton()), + ], + ); + } + + Widget _buildSectionHeader(String title, MotorSignalThemeColors colors) { + return Text( + title, + style: TextStyle( + fontFamily: 'Sora', + fontSize: 12, + fontWeight: FontWeight.w800, + color: colors.cyberCyan, + letterSpacing: 0.1, + ), + ); + } + + Widget _buildMetricRow( + String label1, + String val1, + String label2, + String val2, + ThemeData theme, + MotorSignalThemeColors colors, + ) { + Widget buildItem(String label, String val) { + return Row( + children: [ + Flexible( + flex: 6, + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: 'Inter', + fontSize: 9.5, + fontWeight: FontWeight.w700, + color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + ), + ), + ), + const SizedBox(width: 4), + Expanded( + flex: 5, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + val, + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 11, + fontWeight: FontWeight.w800, + color: theme.colorScheme.onSurface, + ), + ), + ), + ), + ], + ); + } + + return Row( + children: [ + Expanded(child: buildItem(label1, val1)), + const SizedBox(width: 8), + Expanded(child: buildItem(label2, val2)), + ], + ); + } + + Future _confirmDelete(BuildContext context, Ride r) async { + final colors = context.motorColors; + final confirm = await showDialog( + context: context, + builder: (c) => AlertDialog( + title: const Text('Delete Ride?'), + content: const Text('This cannot be undone. Historical telemetry for this ride will be removed.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(c, false), + child: const Text('CANCEL'), + ), + TextButton( + onPressed: () => Navigator.pop(c, true), + child: Text( + 'DELETE', + style: TextStyle(color: colors.raceRed, fontWeight: FontWeight.bold), + ), + ), + ], + ), + ); + + if (confirm == true && mounted) { + final db = ref.read(databaseProvider); + await (db.delete( + db.telemetrySamples, + )..where((t) => t.rideId.equals(r.id))).go(); + await (db.delete( + db.rideEvents, + )..where((e) => e.rideId.equals(r.id))).go(); + await (db.delete( + db.rides, + )..where((ride) => ride.id.equals(r.id))).go(); + if (mounted && context.mounted) { + Navigator.of(context).pop(); + } + } + } } + diff --git a/lib/features/history/widgets/evolution_ride_details_view.dart b/lib/features/history/widgets/evolution_ride_details_view.dart deleted file mode 100644 index 006a254..0000000 --- a/lib/features/history/widgets/evolution_ride_details_view.dart +++ /dev/null @@ -1,1035 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; -import '../../../database/database.dart'; -import '../../../services/export_service.dart'; -import '../../../services/entitlements/entitlement_providers.dart'; -import '../../../services/entitlements/feature_access_service.dart'; -import '../../entitlements/widgets/pro_lock_badge.dart'; -import '../../../services/github/github_sync_service.dart'; -import '../../replay/replay_screen.dart'; -import '../../map/map_screen.dart'; - -class EvolutionRideDetailsView extends ConsumerWidget { - final Ride ride; - final List events; - - const EvolutionRideDetailsView({ - super.key, - required this.ride, - required this.events, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final bikeName = - (ride.motorcycleYear != null && - ride.motorcycleMake != null && - ride.motorcycleModel != null) - ? '${ride.motorcycleYear} ${ride.motorcycleMake} ${ride.motorcycleModel}' - .toUpperCase() - : 'YAMAHA MT-09 SP'; - - final durationSec = ride.endTime != null - ? ride.endTime!.difference(ride.startTime).inSeconds - : (ride.movingTime + ride.stoppedTime); - final durationMin = (durationSec / 60).toStringAsFixed(0); - final movingMin = (ride.movingTime / 60).toStringAsFixed(0); - - final isLandscape = - MediaQuery.of(context).orientation == Orientation.landscape; - final syncService = ref.watch(githubSyncServiceProvider); - final isUploaded = syncService.isRideUploaded(ride.id); - final commitSha = syncService.getRideCommitSha(ride.id); - - return Scaffold( - backgroundColor: Colors.black, - appBar: AppBar( - toolbarHeight: isLandscape ? 36 : 52, - backgroundColor: const Color(0xFF0A0A0A), - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Color(0xFF00E5FF)), - onPressed: () => Navigator.of(context).pop(), - ), - title: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - 'RIDE #${ride.id} TELEMETRY ARCHIVE', - style: TextStyle( - fontFamily: 'Inter', - fontSize: isLandscape ? 12 : 14, - fontWeight: FontWeight.w900, - color: Colors.white, - letterSpacing: 0.1, - ), - ), - ), - actions: [ - IconButton( - icon: const Icon(Icons.share, color: Color(0xFF00E5FF)), - tooltip: 'Export CSV', - onPressed: () async { - final isPro = ref - .read(featureAccessProvider) - .canAccess(MotorSignalFeature.csvExport); - if (!isPro) { - showProUpgradeSheet( - context, - ref, - feature: MotorSignalFeature.csvExport, - ); - return; - } - final exporter = ref.read(exportServiceProvider); - await exporter.exportRide(ride.id, asGpx: false); - }, - ), - ], - ), - body: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // GitHub Sync Status Card - _buildGitHubSyncBanner( - context, - ref, - syncService, - isUploaded, - commitSha, - ), - const SizedBox(height: 12), - - // Top Row: Motorcycle Blueprint & Route Preview Blueprint Cards - Row( - children: [ - // Bike Blueprint Card - Expanded( - child: Container( - height: 130, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(14), - border: Border.all( - color: const Color(0xFF1F1F1F), - width: 1.2, - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.two_wheeler, - color: Colors.white38, - size: 40, - ), - const SizedBox(height: 8), - Text( - bikeName, - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 11, - fontWeight: FontWeight.w800, - color: Colors.white70, - ), - ), - ], - ), - ), - ), - const SizedBox(width: 12), - // Route Preview Blueprint Card (Interactive) - Expanded( - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => MapScreen(rideId: ride.id), - ), - ); - }, - borderRadius: BorderRadius.circular(14), - child: Container( - height: 130, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(14), - border: Border.all( - color: const Color(0xFF00E5FF).withAlpha(100), - width: 1.2, - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.map_rounded, - color: Color(0xFF00E5FF), - size: 36, - ), - const SizedBox(height: 6), - const Text( - 'ROUTE PREVIEW', - style: TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 11, - fontWeight: FontWeight.w900, - color: Color(0xFF00E5FF), - letterSpacing: 0.08, - ), - ), - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 3, - ), - decoration: BoxDecoration( - color: const Color(0xFF00E5FF).withAlpha(40), - borderRadius: BorderRadius.circular(6), - ), - child: const Text( - 'VIEW MAP >', - style: TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 8.5, - fontWeight: FontWeight.w800, - color: Color(0xFF00E5FF), - ), - ), - ), - ], - ), - ), - ), - ), - ), - ], - ), - - const SizedBox(height: 18), - - // Section 1: SUMMARY - _buildSectionHeader('SESSION OVERVIEW'), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(14), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: Column( - children: [ - _buildMetricRow( - 'DISTANCE:', - '${ride.distanceMiles.toStringAsFixed(2)} mi', - 'DURATION:', - '$durationMin min', - ), - const Divider(color: Color(0xFF222222), height: 18), - _buildMetricRow( - 'MOVING TIME:', - '$movingMin min', - 'AVG SPEED:', - '${ride.averageSpeed.toStringAsFixed(1)} mph', - ), - const Divider(color: Color(0xFF222222), height: 18), - _buildMetricRow( - 'TOP SPEED:', - '${ride.topSpeedMph.toStringAsFixed(1)} mph', - 'DATE:', - DateFormat('MM/dd/yyyy').format(ride.startTime.toLocal()), - ), - ], - ), - ), - - const SizedBox(height: 18), - - // Section 2: HANDLING (Responsive Layout) - _buildSectionHeader('HANDLING & DYNAMICS'), - const SizedBox(height: 8), - _buildResponsiveHandlingCards(context, isLandscape), - - const SizedBox(height: 18), - - // Section 3: EVENTS (Interactive Rows) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildSectionHeader('FORENSIC EVENTS (${events.length})'), - if (events.isNotEmpty) - Text( - 'TAP TO INSPECT', - style: TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 9, - fontWeight: FontWeight.w700, - color: const Color(0xFF00E5FF).withValues(alpha: 0.8), - ), - ), - ], - ), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(14), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: events.isEmpty - ? const Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: Center( - child: Text( - 'No threshold handling events registered during this session.', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 12, - color: Colors.white54, - ), - ), - ), - ) - : Column( - children: events - .take(15) - .map( - (event) => - _buildInteractiveEventTile(context, event), - ) - .toList(), - ), - ), - - const SizedBox(height: 24), - - // Section 4: Action Buttons - LayoutBuilder( - builder: (context, constraints) { - final isNarrow = constraints.maxWidth < 390; - - Widget buildReplayButton() => OutlinedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => ReplayScreen(rideId: ride.id), - ), - ); - }, - style: OutlinedButton.styleFrom( - foregroundColor: const Color(0xFF00E5FF), - side: const BorderSide( - color: Color(0xFF00E5FF), - width: 1.5, - ), - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - child: const FittedBox( - fit: BoxFit.scaleDown, - child: Text( - 'REPLAY RIDE', - style: TextStyle( - fontFamily: 'Sora', - fontWeight: FontWeight.w800, - fontSize: 12, - ), - ), - ), - ); - - Widget buildGpxButton() => OutlinedButton( - onPressed: () async { - final exporter = ref.read(exportServiceProvider); - await exporter.exportRide(ride.id, asGpx: true); - }, - style: OutlinedButton.styleFrom( - foregroundColor: Colors.white70, - side: const BorderSide( - color: Color(0xFF333333), - width: 1.2, - ), - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - child: const FittedBox( - fit: BoxFit.scaleDown, - child: Text( - 'EXPORT GPX', - style: TextStyle( - fontFamily: 'Sora', - fontWeight: FontWeight.w700, - fontSize: 12, - ), - ), - ), - ); - - Widget buildCsvButton() => Builder( - builder: (context) { - final isProCsv = ref - .watch(featureAccessProvider) - .canAccess(MotorSignalFeature.csvExport); - return OutlinedButton( - onPressed: () async { - if (!isProCsv) { - showProUpgradeSheet( - context, - ref, - feature: MotorSignalFeature.csvExport, - ); - return; - } - final exporter = ref.read(exportServiceProvider); - await exporter.exportRide(ride.id, asGpx: false); - }, - style: OutlinedButton.styleFrom( - foregroundColor: Colors.white70, - side: const BorderSide( - color: Color(0xFF333333), - width: 1.2, - ), - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - child: FittedBox( - fit: BoxFit.scaleDown, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'EXPORT CSV', - style: TextStyle( - fontFamily: 'Sora', - fontWeight: FontWeight.w700, - fontSize: 12, - ), - ), - if (!isProCsv) ...[ - const SizedBox(width: 6), - const ProBadge( - fontSize: 8, - padding: EdgeInsets.symmetric( - horizontal: 4, - vertical: 1, - ), - ), - ], - ], - ), - ), - ); - }, - ); - - if (isNarrow) { - return Column( - children: [ - Row( - children: [ - Expanded(child: buildReplayButton()), - const SizedBox(width: 8), - Expanded(child: buildGpxButton()), - ], - ), - const SizedBox(height: 8), - SizedBox( - width: double.infinity, - child: buildCsvButton(), - ), - ], - ); - } - - return Row( - children: [ - Expanded(child: buildReplayButton()), - const SizedBox(width: 8), - Expanded(child: buildGpxButton()), - const SizedBox(width: 8), - Expanded(child: buildCsvButton()), - ], - ); - }, - ), - - const SizedBox(height: 24), - ], - ), - ), - ); - } - - Widget _buildGitHubSyncBanner( - BuildContext context, - WidgetRef ref, - GitHubSyncService syncService, - bool isUploaded, - String? commitSha, - ) { - final isSyncing = syncService.state == GitHubUploadState.uploading; - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), - decoration: BoxDecoration( - color: const Color(0xFF0F141C), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isUploaded - ? const Color(0xFF00E676).withValues(alpha: 0.5) - : const Color(0xFF00E5FF).withValues(alpha: 0.4), - width: 1.2, - ), - ), - child: Row( - children: [ - Icon( - isUploaded - ? Icons.cloud_done - : (isSyncing ? Icons.cloud_sync : Icons.cloud_upload_outlined), - color: isUploaded - ? const Color(0xFF00E676) - : const Color(0xFF00E5FF), - size: 24, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - isUploaded - ? 'GITHUB LOGS SYNCHRONIZED' - : (isSyncing - ? 'UPLOADING TO GITHUB...' - : 'GITHUB LOG SYNC'), - style: TextStyle( - fontFamily: 'Sora', - fontSize: 11, - fontWeight: FontWeight.w800, - color: isUploaded ? const Color(0xFF00E676) : Colors.white, - ), - ), - Text( - isUploaded - ? 'Commit: ${commitSha != null && commitSha.length >= 7 ? commitSha.substring(0, 7) : "synced"}' - : 'Repository: bponders12-cell/ride_logs', - style: const TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 9.5, - color: Colors.white54, - ), - ), - ], - ), - ), - if (!isUploaded) - TextButton( - onPressed: isSyncing - ? null - : () async { - final result = await syncService.uploadRide( - ride.id, - isManual: true, - ); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(result.message), - backgroundColor: result.success - ? const Color(0xFF00E676) - : Colors.redAccent, - ), - ); - } - }, - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 4, - ), - backgroundColor: const Color( - 0xFF00E5FF, - ).withValues(alpha: 0.15), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: Text( - isSyncing ? 'UPLOADING...' : 'UPLOAD NOW', - style: const TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 10, - fontWeight: FontWeight.w800, - color: Color(0xFF00E5FF), - ), - ), - ), - ], - ), - ); - } - - Widget _buildResponsiveHandlingCards(BuildContext context, bool isLandscape) { - final cards = [ - _buildHandlingKpiCard( - 'MAX LEAN (LEFT)', - '${ride.maxLeanLeft.toStringAsFixed(1)}°', - Icons.turn_left, - const Color(0xFF00E5FF), - ), - _buildHandlingKpiCard( - 'MAX LEAN (RIGHT)', - '${ride.maxLeanRight.toStringAsFixed(1)}°', - Icons.turn_right, - const Color(0xFF00E5FF), - ), - _buildHandlingKpiCard( - 'MAX ACCELERATION', - '${ride.maxAccelG.toStringAsFixed(2)} G', - Icons.speed, - const Color(0xFF76FF03), - ), - _buildHandlingKpiCard( - 'MAX BRAKING', - '${ride.maxBrakingG.toStringAsFixed(2)} G', - Icons.pan_tool_alt, - const Color(0xFFFF5252), - ), - ]; - - if (isLandscape) { - return Row( - children: cards - .map( - (c) => Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: c, - ), - ), - ) - .toList(), - ); - } - - return Column( - children: [ - Row( - children: [ - Expanded(child: cards[0]), - const SizedBox(width: 8), - Expanded(child: cards[1]), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded(child: cards[2]), - const SizedBox(width: 8), - Expanded(child: cards[3]), - ], - ), - ], - ); - } - - Widget _buildHandlingKpiCard( - String label, - String value, - IconData icon, - Color accentColor, - ) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 9.5, - fontWeight: FontWeight.w700, - color: Colors.white54, - ), - ), - ), - Icon(icon, color: accentColor, size: 16), - ], - ), - const SizedBox(height: 6), - Text( - value, - style: const TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 18, - fontWeight: FontWeight.w900, - color: Colors.white, - ), - ), - ], - ), - ); - } - - Widget _buildInteractiveEventTile(BuildContext context, RideEvent event) { - Color badgeColor = Colors.amberAccent; - IconData eventIcon = Icons.warning_amber_rounded; - - if (event.eventType.toLowerCase().contains('accel')) { - badgeColor = const Color(0xFF76FF03); - eventIcon = Icons.arrow_upward; - } else if (event.eventType.toLowerCase().contains('brak')) { - badgeColor = const Color(0xFFFF5252); - eventIcon = Icons.arrow_downward; - } else if (event.eventType.toLowerCase().contains('corner') || - event.eventType.toLowerCase().contains('lean')) { - badgeColor = const Color(0xFF00E5FF); - eventIcon = Icons.cached; - } - - String formattedValue = '${event.value.toStringAsFixed(1)}°'; - if (event.eventType.toLowerCase().contains('accel') || - event.eventType.toLowerCase().contains('brak')) { - formattedValue = '${event.value.toStringAsFixed(2)} G'; - } - - final timeStr = DateFormat( - 'HH:mm:ss.SSS', - ).format(event.timestamp.toLocal()); - - return Material( - color: Colors.transparent, - child: InkWell( - onTap: () => _showEventDetailsDialog( - context, - event, - formattedValue, - timeStr, - badgeColor, - ), - borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 6), - child: Row( - children: [ - Icon(eventIcon, color: badgeColor, size: 16), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - event.eventType.toUpperCase(), - style: const TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 11, - fontWeight: FontWeight.w800, - color: Colors.white, - ), - ), - Text( - timeStr, - style: const TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 9, - color: Colors.white38, - ), - ), - ], - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: badgeColor.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - formattedValue, - style: TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 11, - fontWeight: FontWeight.w700, - color: badgeColor, - ), - ), - ), - const SizedBox(width: 6), - const Icon(Icons.chevron_right, color: Colors.white24, size: 16), - ], - ), - ), - ), - ); - } - - void _showEventDetailsDialog( - BuildContext context, - RideEvent event, - String formattedValue, - String timeStr, - Color badgeColor, - ) { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - backgroundColor: const Color(0xFF111111), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: BorderSide( - color: badgeColor.withValues(alpha: 0.5), - width: 1.2, - ), - ), - title: Row( - children: [ - Icon(Icons.insights, color: badgeColor, size: 20), - const SizedBox(width: 8), - Expanded( - child: Text( - event.eventType.toUpperCase(), - style: const TextStyle( - fontFamily: 'Sora', - fontSize: 14, - fontWeight: FontWeight.w900, - color: Colors.white, - ), - ), - ), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildDialogRow('Timestamp:', timeStr), - _buildDialogRow('Magnitude / Value:', formattedValue), - if (event.latitude != null && event.longitude != null) - _buildDialogRow( - 'GPS Coordinates:', - '${event.latitude!.toStringAsFixed(6)}, ${event.longitude!.toStringAsFixed(6)}', - ), - _buildDialogRow( - 'Event ID / Seq:', - '#${event.id} (Ride #${event.rideId})', - ), - const SizedBox(height: 12), - Container( - width: double.infinity, - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: const Color(0xFF0A0A0A), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: const Color(0xFF222222)), - ), - child: const Text( - 'Authoritative telemetry sampled at 20 Hz. Moving metrics gated at speed >= 3.0 MPH.', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 10, - color: Colors.white54, - ), - ), - ), - ], - ), - actions: [ - if (event.latitude != null && event.longitude != null) - TextButton.icon( - icon: const Icon(Icons.map, color: Color(0xFF00E5FF), size: 16), - label: const Text( - 'VIEW ON MAP', - style: TextStyle( - color: Color(0xFF00E5FF), - fontFamily: 'JetBrains Mono', - fontWeight: FontWeight.bold, - ), - ), - onPressed: () { - Navigator.pop(ctx); - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => MapScreen(rideId: event.rideId), - ), - ); - }, - ), - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text( - 'CLOSE', - style: TextStyle( - color: Colors.white70, - fontFamily: 'JetBrains Mono', - ), - ), - ), - ], - ), - ); - } - - Widget _buildDialogRow(String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 11, - color: Colors.white54, - ), - ), - Flexible( - child: Text( - value, - textAlign: TextAlign.right, - style: const TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 11, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ], - ), - ); - } - - Widget _buildSectionHeader(String title) { - return Text( - title, - style: const TextStyle( - fontFamily: 'Sora', - fontSize: 12, - fontWeight: FontWeight.w800, - color: Colors.white, - letterSpacing: 0.1, - ), - ); - } - - Widget _buildMetricRow( - String label1, - String value1, - String label2, - String value2, - ) { - Widget buildItem(String label, String value) { - return LayoutBuilder( - builder: (context, constraints) { - if (constraints.maxWidth < 135) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 10, - fontWeight: FontWeight.w700, - color: Colors.white54, - ), - ), - const SizedBox(height: 2), - FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - value, - style: const TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 12, - fontWeight: FontWeight.w800, - color: Colors.white, - ), - ), - ), - ], - ); - } - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 11, - fontWeight: FontWeight.w700, - color: Colors.white54, - ), - ), - ), - const SizedBox(width: 4), - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - value, - style: const TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 12, - fontWeight: FontWeight.w800, - color: Colors.white, - ), - ), - ), - ], - ); - }, - ); - } - - return Row( - children: [ - Expanded(child: buildItem(label1, value1)), - if (label2.isNotEmpty) ...[ - const SizedBox(width: 12), - Expanded(child: buildItem(label2, value2)), - ], - ], - ); - } -} diff --git a/lib/features/records/evolution_records_screen.dart b/lib/features/records/evolution_records_screen.dart deleted file mode 100644 index d16729e..0000000 --- a/lib/features/records/evolution_records_screen.dart +++ /dev/null @@ -1,307 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'records_service.dart'; - -class EvolutionRecordsScreen extends ConsumerWidget { - const EvolutionRecordsScreen({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final records = ref.watch(recordsServiceProvider); - - final topSpeed = records.topSpeedMph > 0 ? records.topSpeedMph : 164.0; - final zeroToSixty = records.fastest0To60 < 20 ? records.fastest0To60 : 2.9; - final zeroToHundred = records.fastest0To100 < 30 ? records.fastest0To100 : 6.4; - final leanLeft = records.maxLeanLeft > 0 ? records.maxLeanLeft : 48.0; - final leanRight = records.maxLeanRight > 0 ? records.maxLeanRight : 50.0; - final maxAccel = records.maxAccelG > 0 ? records.maxAccelG : 1.2; - final maxBrake = records.maxBrakingG > 0 ? records.maxBrakingG : 1.4; - - final isLandscape = MediaQuery.of(context).orientation == Orientation.landscape; - - return Scaffold( - backgroundColor: Colors.black, - appBar: AppBar( - toolbarHeight: isLandscape ? 36 : 52, - backgroundColor: Colors.transparent, - elevation: 0, - centerTitle: true, - title: isLandscape - ? const Text( - 'MOTOR SIGNAL // PERSONAL RECORDS', - style: TextStyle( - fontFamily: 'Sora', - fontSize: 13, - fontWeight: FontWeight.w900, - color: Colors.white, - letterSpacing: 0.12, - ), - ) - : const Column( - children: [ - Text( - 'MOTOR SIGNAL', - style: TextStyle( - fontFamily: 'Sora', - fontSize: 18, - fontWeight: FontWeight.w900, - color: Colors.white, - letterSpacing: 0.15, - ), - ), - Text( - 'PERSONAL RECORDS', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 11, - fontWeight: FontWeight.w700, - color: Colors.white54, - letterSpacing: 0.1, - ), - ), - ], - ), - ), - body: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Section 1: SPEED - _buildSectionHeader('SPEED'), - const SizedBox(height: 8), - Container( - width: double.infinity, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'TOP SPEED', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 12, - fontWeight: FontWeight.w800, - color: Colors.white70, - letterSpacing: 0.08, - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: const Color(0xFF00363D), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: const Color(0xFF00E5FF), width: 0.8), - ), - child: const Text( - 'NEW RECORD', - style: TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 10, - fontWeight: FontWeight.w800, - color: Color(0xFF00E5FF), - letterSpacing: 0.05, - ), - ), - ), - ], - ), - const SizedBox(height: 12), - Center( - child: Text( - '${topSpeed.toStringAsFixed(0)} MPH', - style: const TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 64, - fontWeight: FontWeight.w900, - color: Colors.white, - letterSpacing: -0.02, - ), - ), - ), - ], - ), - ), - - const SizedBox(height: 20), - - // Section 2: PERFORMANCE TIMES - _buildSectionHeader('PERFORMANCE TIMES'), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: Row( - children: [ - Expanded( - child: _buildBenchmarkCard('0-60', '${zeroToSixty.toStringAsFixed(1)} s', zeroToSixty, 5.0), - ), - Container(width: 1, height: 64, color: const Color(0xFF222222)), - Expanded( - child: _buildBenchmarkCard('0-100', '${zeroToHundred.toStringAsFixed(1)} s', zeroToHundred, 10.0), - ), - ], - ), - ), - - const SizedBox(height: 20), - - // Section 3: HANDLING - _buildSectionHeader('HANDLING'), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: Column( - children: [ - Row( - children: [ - Expanded(child: _buildHandlingCell('MAX LEAN LEFT', '${leanLeft.toStringAsFixed(0)}°')), - Container(width: 1, height: 60, color: const Color(0xFF222222)), - Expanded(child: _buildHandlingCell('MAX LEAN RIGHT', '${leanRight.toStringAsFixed(0)}°')), - ], - ), - const Divider(color: Color(0xFF222222), height: 24), - Row( - children: [ - Expanded(child: _buildHandlingCell('MAX ACCEL', '${maxAccel.toStringAsFixed(1)} G')), - Container(width: 1, height: 60, color: const Color(0xFF222222)), - Expanded(child: _buildHandlingCell('MAX BRAKING Gs', '${maxBrake.toStringAsFixed(1)} G')), - ], - ), - ], - ), - ), - - const SizedBox(height: 24), - ], - ), - ), - ); - } - - Widget _buildSectionHeader(String title) { - return Center( - child: Text( - title, - style: const TextStyle( - fontFamily: 'Sora', - fontSize: 13, - fontWeight: FontWeight.w800, - color: Colors.white, - letterSpacing: 0.12, - ), - ), - ); - } - - Widget _buildBenchmarkCard(String label, String value, double time, double maxScale) { - final frac = (time / maxScale).clamp(0.1, 1.0); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 12, - fontWeight: FontWeight.w700, - color: Colors.white54, - ), - ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - value, - style: const TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 32, - fontWeight: FontWeight.w900, - color: Colors.white, - ), - ), - Container( - width: 6, - height: 14, - decoration: BoxDecoration( - color: const Color(0xFF00E5FF), - borderRadius: BorderRadius.circular(2), - ), - ), - ], - ), - const SizedBox(height: 4), - LinearProgressIndicator( - value: frac, - backgroundColor: const Color(0xFF1F1F1F), - valueColor: const AlwaysStoppedAnimation(Color(0xFF00E5FF)), - minHeight: 2, - ), - ], - ), - ); - } - - Widget _buildHandlingCell(String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 11, - fontWeight: FontWeight.w700, - color: Colors.white54, - ), - ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - value, - style: const TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 28, - fontWeight: FontWeight.w900, - color: Colors.white, - ), - ), - Container( - width: 6, - height: 12, - decoration: BoxDecoration( - color: const Color(0xFF00E5FF), - borderRadius: BorderRadius.circular(2), - ), - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/features/records/records_screen.dart b/lib/features/records/records_screen.dart index 76e3d3b..b286efd 100644 --- a/lib/features/records/records_screen.dart +++ b/lib/features/records/records_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../core/theme/evolution_theme_config.dart'; -import 'evolution_records_screen.dart'; +import '../../core/responsive/responsive_layout.dart'; +import '../../core/theme/theme_extensions.dart'; import 'records_service.dart'; class RecordsScreen extends ConsumerWidget { @@ -9,113 +9,511 @@ class RecordsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final themeMode = ref.watch(dashboardThemeModeProvider); - if (themeMode == DashboardThemeMode.evolution) { - return const EvolutionRecordsScreen(); - } - final records = ref.watch(recordsServiceProvider); + final colors = context.motorColors; return Scaffold( appBar: AppBar( - title: const Text('Personal Records'), + title: const Text( + 'Personal Records', + style: TextStyle(fontWeight: FontWeight.bold), + ), actions: [ IconButton( - icon: const Icon(Icons.emoji_events, color: Colors.amberAccent), + icon: Icon(Icons.emoji_events, color: colors.warningAmber), + tooltip: 'Trophies', onPressed: () {}, ), ], ), - body: ListView( - padding: const EdgeInsets.all(16), + body: ResponsiveLayout( + builder: (context, responsive) { + final isMultiColumn = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape); + + if (isMultiColumn) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader('SPEED', colors), + const SizedBox(height: 8), + _buildHeroTopSpeedCard(context, records.topSpeedMph, colors), + const SizedBox(height: 16), + _buildSectionHeader('PERFORMANCE TIMES', colors), + const SizedBox(height: 8), + _buildPerformanceTimesCard(context, records, colors), + ], + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader('HANDLING & DYNAMICS', colors), + const SizedBox(height: 8), + _buildHandlingCard(context, records, colors), + const SizedBox(height: 16), + _buildSectionHeader('ENDURANCE & ROUTE', colors), + const SizedBox(height: 8), + _buildEnduranceCard(context, records, colors), + ], + ), + ), + ], + ), + ); + } + + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader('SPEED', colors), + const SizedBox(height: 8), + _buildHeroTopSpeedCard(context, records.topSpeedMph, colors), + const SizedBox(height: 18), + _buildSectionHeader('PERFORMANCE TIMES', colors), + const SizedBox(height: 8), + _buildPerformanceTimesCard(context, records, colors), + const SizedBox(height: 18), + _buildSectionHeader('HANDLING & DYNAMICS', colors), + const SizedBox(height: 8), + _buildHandlingCard(context, records, colors), + const SizedBox(height: 18), + _buildSectionHeader('ENDURANCE & ROUTE', colors), + const SizedBox(height: 8), + _buildEnduranceCard(context, records, colors), + const SizedBox(height: 24), + ], + ), + ); + }, + ), + ); + } + + Widget _buildSectionHeader(String title, MotorSignalThemeColors colors) { + return Text( + title, + style: TextStyle( + fontFamily: 'Sora', + fontSize: 12, + fontWeight: FontWeight.w800, + color: colors.cyberCyan, + letterSpacing: 0.1, + ), + ); + } + + Widget _buildHeroTopSpeedCard(BuildContext context, double topSpeedMph, MotorSignalThemeColors colors) { + final hasRecord = topSpeedMph > 0; + final theme = Theme.of(context); + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: colors.surfaceContainer, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: colors.outlineVariant, width: 1.2), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionHeader('SPEED'), - _buildRecordItem('Top Speed', records.topSpeedMph, 'MPH', isTime: false), - const SizedBox(height: 16), - - _buildSectionHeader('PERFORMANCE TIMES'), - _buildRecordItem('Fastest 0-30', records.fastest0To30, 's', isTime: true), - _buildRecordItem('Fastest 0-60', records.fastest0To60, 's', isTime: true), - _buildRecordItem('Fastest 0-100', records.fastest0To100, 's', isTime: true), - _buildRecordItem('Fastest 60-100', records.fastest60To100, 's', isTime: true), - const SizedBox(height: 16), - - _buildSectionHeader('HANDLING'), Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: _buildRecordItem('Max Lean (Left)', records.maxLeanLeft, '°', isTime: false)), - Expanded(child: _buildRecordItem('Max Lean (Right)', records.maxLeanRight, '°', isTime: false)), + Text( + 'TOP SPEED', + style: TextStyle( + fontFamily: 'Inter', + fontSize: 12, + fontWeight: FontWeight.w800, + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), + letterSpacing: 0.08, + ), + ), + if (hasRecord) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: colors.cyberCyan.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: colors.cyberCyan, width: 0.8), + ), + child: Text( + 'RECORD', + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 10, + fontWeight: FontWeight.w800, + color: colors.cyberCyan, + letterSpacing: 0.05, + ), + ), + ), ], ), - const SizedBox(height: 8), - Row( - children: [ - Expanded(child: _buildRecordItem('Max Accel', records.maxAccelG, 'G', isTime: false)), - Expanded(child: _buildRecordItem('Max Braking', records.maxBrakingG, 'G', isTime: false)), - ], + const SizedBox(height: 12), + Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + hasRecord ? topSpeedMph.toStringAsFixed(0) : '--', + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 54, + fontWeight: FontWeight.w900, + color: theme.colorScheme.onSurface, + letterSpacing: -0.02, + ), + ), + const SizedBox(width: 8), + Text( + 'MPH', + style: TextStyle( + fontFamily: 'Sora', + fontSize: 18, + fontWeight: FontWeight.w800, + color: colors.cyberCyan, + ), + ), + ], + ), + ), ), - const SizedBox(height: 16), - - _buildSectionHeader('ENDURANCE & ROUTE'), - _buildRecordItem('Longest Ride', records.longestRideMiles, 'Miles', isTime: false), - _buildRecordItem('Greatest Elevation Gain', records.greatestElevationGain, 'm', isTime: false), ], ), ); } - Widget _buildSectionHeader(String title) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text( - title, - style: const TextStyle( - color: Colors.cyanAccent, - fontSize: 14, - fontWeight: FontWeight.bold, - letterSpacing: 2, - ), + Widget _buildPerformanceTimesCard( + BuildContext context, + PersonalRecords records, + MotorSignalThemeColors colors, + ) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colors.surfaceContainer, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: colors.outlineVariant, width: 1.2), + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: _buildBenchmarkCell('0-30', records.fastest0To30, 3.0, colors, context), + ), + Container(width: 1, height: 56, color: colors.outlineVariant), + Expanded( + child: _buildBenchmarkCell('0-60', records.fastest0To60, 6.0, colors, context), + ), + ], + ), + Divider(color: colors.outlineVariant, height: 20), + Row( + children: [ + Expanded( + child: _buildBenchmarkCell('0-100', records.fastest0To100, 12.0, colors, context), + ), + Container(width: 1, height: 56, color: colors.outlineVariant), + Expanded( + child: _buildBenchmarkCell('60-100', records.fastest60To100, 8.0, colors, context), + ), + ], + ), + ], ), ); } - Widget _buildRecordItem(String label, double value, String unit, {required bool isTime}) { - // If it's a time record and it's infinity, it means no record yet. - // If it's a normal record and it's 0, it means no record yet. - bool hasRecord = isTime ? value != double.infinity : value > 0; - - return Card( - color: Colors.white10, - margin: const EdgeInsets.only(bottom: 8, right: 8), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: const TextStyle(color: Colors.white70)), - const SizedBox(height: 4), - Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text( - hasRecord ? (isTime ? value.toStringAsFixed(2) : value.toStringAsFixed(1)) : '--', - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.white, + Widget _buildBenchmarkCell( + String label, + double value, + double maxScale, + MotorSignalThemeColors colors, + BuildContext context, + ) { + final hasRecord = value != double.infinity && value > 0; + final frac = hasRecord ? (value / maxScale).clamp(0.1, 1.0) : 0.0; + final theme = Theme.of(context); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontFamily: 'Inter', + fontSize: 11.5, + fontWeight: FontWeight.w700, + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + hasRecord ? '${value.toStringAsFixed(2)} s' : '--', + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 22, + fontWeight: FontWeight.w900, + color: theme.colorScheme.onSurface, + ), ), ), + ), + if (hasRecord) ...[ const SizedBox(width: 4), - Text( - unit, - style: const TextStyle(color: Colors.white54, fontSize: 14), + Container( + width: 6, + height: 12, + decoration: BoxDecoration( + color: colors.cyberCyan, + borderRadius: BorderRadius.circular(2), + ), ), ], + ], + ), + const SizedBox(height: 4), + LinearProgressIndicator( + value: frac, + backgroundColor: colors.outlineVariant, + valueColor: AlwaysStoppedAnimation(colors.cyberCyan), + minHeight: 2, + ), + ], + ), + ); + } + + Widget _buildHandlingCard( + BuildContext context, + PersonalRecords records, + MotorSignalThemeColors colors, + ) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colors.surfaceContainer, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: colors.outlineVariant, width: 1.2), + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: _buildHandlingCell( + 'MAX LEAN LEFT', + records.maxLeanLeft > 0 ? '${records.maxLeanLeft.toStringAsFixed(1)}°' : '--', + colors.leanAngleLeft, + context, + ), + ), + Container(width: 1, height: 50, color: colors.outlineVariant), + Expanded( + child: _buildHandlingCell( + 'MAX LEAN RIGHT', + records.maxLeanRight > 0 ? '${records.maxLeanRight.toStringAsFixed(1)}°' : '--', + colors.leanAngleRight, + context, + ), + ), + ], + ), + Divider(color: colors.outlineVariant, height: 20), + Row( + children: [ + Expanded( + child: _buildHandlingCell( + 'MAX ACCEL', + records.maxAccelG > 0 ? '${records.maxAccelG.toStringAsFixed(2)} G' : '--', + colors.gForcePositive, + context, + ), + ), + Container(width: 1, height: 50, color: colors.outlineVariant), + Expanded( + child: _buildHandlingCell( + 'MAX BRAKING', + records.maxBrakingG > 0 ? '${records.maxBrakingG.toStringAsFixed(2)} G' : '--', + colors.gForceNegative, + context, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildHandlingCell( + String label, + String value, + Color accentColor, + BuildContext context, + ) { + final theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontFamily: 'Inter', + fontSize: 10.5, + fontWeight: FontWeight.w700, + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), ), - ], - ), + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + value, + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 20, + fontWeight: FontWeight.w900, + color: theme.colorScheme.onSurface, + ), + ), + ), + ), + const SizedBox(width: 4), + Container( + width: 6, + height: 10, + decoration: BoxDecoration( + color: accentColor, + borderRadius: BorderRadius.circular(2), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildEnduranceCard( + BuildContext context, + PersonalRecords records, + MotorSignalThemeColors colors, + ) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colors.surfaceContainer, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: colors.outlineVariant, width: 1.2), + ), + child: Row( + children: [ + Expanded( + child: _buildEnduranceCell( + 'LONGEST RIDE', + records.longestRideMiles > 0 ? '${records.longestRideMiles.toStringAsFixed(1)} mi' : '--', + Icons.route, + colors, + context, + ), + ), + Container(width: 1, height: 50, color: colors.outlineVariant), + Expanded( + child: _buildEnduranceCell( + 'MAX ELEVATION GAIN', + records.greatestElevationGain > 0 ? '${records.greatestElevationGain.toStringAsFixed(0)} m' : '--', + Icons.terrain, + colors, + context, + ), + ), + ], + ), + ); + } + + Widget _buildEnduranceCell( + String label, + String value, + IconData icon, + MotorSignalThemeColors colors, + BuildContext context, + ) { + final theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, size: 14, color: colors.cyberCyan), + const SizedBox(width: 4), + Flexible( + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: 'Inter', + fontSize: 10.5, + fontWeight: FontWeight.w700, + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + ), + ], + ), + const SizedBox(height: 4), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + value, + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 20, + fontWeight: FontWeight.w900, + color: theme.colorScheme.onSurface, + ), + ), + ), + ], ), ); } } + diff --git a/test/canonical_history_records_garage_responsive_test.dart b/test/canonical_history_records_garage_responsive_test.dart new file mode 100644 index 0000000..3762ea5 --- /dev/null +++ b/test/canonical_history_records_garage_responsive_test.dart @@ -0,0 +1,322 @@ +import 'dart:convert'; +import 'package:drift/drift.dart' as drift; +import 'package:drift/native.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:motorcycle_dash/core/theme/theme.dart'; +import 'package:motorcycle_dash/database/database.dart'; +import 'package:motorcycle_dash/models/motorcycle_models.dart'; +import 'package:motorcycle_dash/services/github/github_sync_service.dart'; +import 'package:motorcycle_dash/services/motorcycle/motorcycle_repository.dart'; +import 'package:motorcycle_dash/features/garage/garage_providers.dart'; +import 'package:motorcycle_dash/features/history/history_screen.dart'; +import 'package:motorcycle_dash/features/history/ride_detail_screen.dart'; +import 'package:motorcycle_dash/features/records/records_screen.dart'; +import 'package:motorcycle_dash/features/records/records_service.dart'; +import 'package:motorcycle_dash/features/garage/garage_screen.dart'; +import 'package:motorcycle_dash/features/garage/motorcycle_detail_screen.dart'; +import 'package:motorcycle_dash/features/garage/add_motorcycle_screen.dart'; + +class _FakeGitHubSyncService extends ChangeNotifier implements GitHubSyncService { + @override + GitHubUploadState get state => GitHubUploadState.idle; + @override + String get statusMessage => ''; + @override + Set get queuedRideIds => const {}; + @override + bool isRideUploaded(int rideId) => false; + @override + String? getRideCommitSha(int rideId) => null; + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _FakeMotorcycleRepository implements MotorcycleRepository { + @override + Future> getYears() async => [ + MotorcycleYear(2024), + MotorcycleYear(2023), + ]; + + @override + Future> getMakes(int year) async => [ + MotorcycleMake(id: 'yamaha', name: 'Yamaha'), + MotorcycleMake(id: 'ducati', name: 'Ducati'), + ]; + + @override + Future> getModels(int year, String makeId) async => [ + MotorcycleModel(id: 'r1', name: 'YZF-R1', makeId: makeId), + MotorcycleModel(id: 'mt09', name: 'MT-09', makeId: makeId), + ]; + + @override + Future> getVariants(int year, String makeId, String modelId) async => [ + MotorcycleVariant(id: 'std', name: 'Standard'), + ]; + + @override + Future getSpecifications(MotorcycleSelection selection) async => + MotorcycleSpecification( + year: 2024, + manufacturer: 'Yamaha', + model: 'YZF-R1', + displacementCc: 998, + horsepower: 198, + torqueLbFt: 83.0, + wetWeightLbs: 448, + claimedTopSpeedMph: 186, + claimedZeroToSixtySec: 2.8, + ); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late AppDatabase db; + late SharedPreferences prefs; + + setUp(() async { + db = AppDatabase.forTesting(NativeDatabase.memory()); + SharedPreferences.setMockInitialValues({ + 'topSpeedMph': 152.0, + 'fastest0To60': 3.1, + 'fastest0To100': 6.8, + 'maxLeanLeft': 44.0, + 'maxLeanRight': 46.0, + 'maxAccelG': 0.88, + 'maxBrakingG': 0.94, + 'longestRideMiles': 120.5, + 'greatestElevationGain': 1450.0, + }); + prefs = await SharedPreferences.getInstance(); + + // Seed test garage motorcycle + final spec = MotorcycleSpecification( + year: 2024, + manufacturer: 'Yamaha', + model: 'YZF-R1', + displacementCc: 998, + horsepower: 198, + torqueLbFt: 83.0, + wetWeightLbs: 448, + claimedTopSpeedMph: 186, + claimedZeroToSixtySec: 2.8, + ); + + await db.into(db.garageMotorcycles).insert( + GarageMotorcyclesCompanion.insert( + year: 2024, + makeId: 'yamaha', + makeName: 'Yamaha', + modelId: 'r1', + modelName: 'YZF-R1', + nickname: const drift.Value('Blue Thunder'), + isDefault: const drift.Value(true), + dateAdded: DateTime.now(), + lastUpdated: DateTime.now(), + specificationJson: drift.Value(json.encode(spec.toJson())), + ), + ); + + // Seed test ride + await db.into(db.rides).insert( + RidesCompanion.insert( + startTime: DateTime(2026, 1, 1, 10, 0), + endTime: drift.Value(DateTime(2026, 1, 1, 10, 45)), + distanceMiles: const drift.Value(25.5), + topSpeedMph: const drift.Value(65.0), + movingTime: const drift.Value(2400), + stoppedTime: const drift.Value(300), + distance: const drift.Value(25.5), + topSpeed: const drift.Value(65.0), + averageSpeed: const drift.Value(42.0), + elevationGain: const drift.Value(350.0), + elevationLoss: const drift.Value(300.0), + maxLeanLeft: const drift.Value(34.0), + maxLeanRight: const drift.Value(36.0), + maxAccelG: const drift.Value(0.75), + maxBrakingG: const drift.Value(0.85), + motorcycleId: const drift.Value(1), + motorcycleYear: const drift.Value(2024), + motorcycleMake: const drift.Value('Yamaha'), + motorcycleModel: const drift.Value('YZF-R1'), + ), + ); + }); + + tearDown(() async { + await db.close(); + }); + + final testViewports = { + 'Compact Phone (320x640)': const Size(320, 640), + 'Standard Phone (360x800)': const Size(360, 800), + 'Modern Phone (390x844)': const Size(390, 844), + 'Large Phone (412x915)': const Size(412, 915), + 'Compact Landscape (640x320)': const Size(640, 320), + 'Standard Landscape (800x360)': const Size(800, 360), + 'Wide Landscape (844x390)': const Size(844, 390), + 'Tablet Portrait (768x1024)': const Size(768, 1024), + 'Tablet Landscape (1024x768)': const Size(1024, 768), + }; + + Widget buildTestApp(Widget child, {ThemeMode mode = ThemeMode.dark}) { + return ProviderScope( + overrides: [ + databaseProvider.overrideWithValue(db), + sharedPreferencesProvider.overrideWithValue(prefs), + githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()), + motorcycleRepositoryProvider.overrideWith((ref) => _FakeMotorcycleRepository()), + ], + child: MaterialApp( + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: mode, + home: child, + ), + ); + } + + group('UI-2D: 9-Viewport Matrix Tests', () { + for (final entry in testViewports.entries) { + final name = entry.key; + final size = entry.value; + + testWidgets('HistoryScreen renders cleanly on $name', (tester) async { + tester.view.physicalSize = size; + 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.text('Ride History'), findsOneWidget); + expect(find.byKey(const ValueKey('history_ride_card_1')), findsOneWidget); + + await tester.pumpWidget(const SizedBox()); + await tester.pump(const Duration(milliseconds: 100)); + }); + + testWidgets('RideDetailScreen renders cleanly on $name', (tester) async { + tester.view.physicalSize = size; + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + final ride = Ride( + id: 1, + startTime: DateTime(2026, 1, 1, 10, 0), + endTime: DateTime(2026, 1, 1, 10, 45), + distanceMiles: 25.5, + topSpeedMph: 65.0, + movingTime: 2400, + stoppedTime: 300, + distance: 25.5, + topSpeed: 65.0, + averageSpeed: 42.0, + elevationGain: 350.0, + elevationLoss: 300.0, + maxLeanLeft: 34.0, + maxLeanRight: 36.0, + maxAccelG: 0.75, + maxBrakingG: 0.85, + motorcycleYear: 2024, + motorcycleMake: 'Yamaha', + motorcycleModel: 'YZF-R1', + ); + + await tester.pumpWidget(buildTestApp(RideDetailScreen(ride: ride))); + await tester.pump(const Duration(milliseconds: 50)); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + expect(find.text('REPLAY RIDE'), findsOneWidget); + expect(find.text('EXPORT GPX'), findsOneWidget); + expect(find.text('EXPORT CSV'), findsOneWidget); + expect(find.text('SESSION OVERVIEW'), findsOneWidget); + + await tester.pumpWidget(const SizedBox()); + await tester.pump(const Duration(milliseconds: 100)); + }); + + testWidgets('RecordsScreen renders cleanly on $name', (tester) async { + tester.view.physicalSize = size; + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + await tester.pumpWidget(buildTestApp(const RecordsScreen())); + await tester.pump(const Duration(milliseconds: 50)); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + expect(find.text('Personal Records'), findsOneWidget); + expect(find.text('TOP SPEED'), findsOneWidget); + expect(find.text('152'), findsOneWidget); + expect(find.text('0-60'), findsOneWidget); + + await tester.pumpWidget(const SizedBox()); + await tester.pump(const Duration(milliseconds: 100)); + }); + + testWidgets('GarageScreen renders cleanly on $name', (tester) async { + tester.view.physicalSize = size; + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + await tester.pumpWidget(buildTestApp(const GarageScreen())); + await tester.pump(const Duration(milliseconds: 50)); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + expect(find.text('My Garage'), findsOneWidget); + expect(find.textContaining('Yamaha YZF-R1'), findsOneWidget); + + await tester.pumpWidget(const SizedBox()); + await tester.pump(const Duration(milliseconds: 100)); + }); + + testWidgets('MotorcycleDetailScreen renders cleanly on $name', (tester) async { + tester.view.physicalSize = size; + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + await tester.pumpWidget(buildTestApp(const MotorcycleDetailScreen(motorcycleId: 1))); + await tester.pump(const Duration(milliseconds: 50)); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + expect(find.text('MY PERFORMANCE'), findsOneWidget); + expect(find.text('SPECIFICATIONS'), findsOneWidget); + + await tester.pumpWidget(const SizedBox()); + await tester.pump(const Duration(milliseconds: 100)); + }); + + testWidgets('AddMotorcycleScreen renders cleanly on $name', (tester) async { + tester.view.physicalSize = size; + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + await tester.pumpWidget(buildTestApp(const AddMotorcycleScreen())); + await tester.pump(const Duration(milliseconds: 50)); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + expect(find.text('Select Year'), findsOneWidget); + expect(find.text('2024'), findsOneWidget); + + await tester.pumpWidget(const SizedBox()); + await tester.pump(const Duration(milliseconds: 100)); + }); + } + }); +} + diff --git a/test/evolution_theme_test.dart b/test/evolution_theme_test.dart index 42fc1db..e00b351 100644 --- a/test/evolution_theme_test.dart +++ b/test/evolution_theme_test.dart @@ -5,7 +5,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:motorcycle_dash/core/theme/evolution_theme_config.dart'; import 'package:motorcycle_dash/features/records/records_service.dart'; import 'package:motorcycle_dash/features/dashboard/widgets/evolution_portrait_dashboard.dart'; -import 'package:motorcycle_dash/features/records/evolution_records_screen.dart'; +import 'package:motorcycle_dash/features/records/records_screen.dart'; import 'package:motorcycle_dash/features/rides/ride_state_machine.dart'; void main() { @@ -78,12 +78,17 @@ void main() { expect(find.text('START SESSION'), findsOneWidget); }); - testWidgets('EvolutionRecordsScreen renders speed records and performance times', (tester) async { + testWidgets('RecordsScreen renders speed records and performance times', (tester) async { tester.view.physicalSize = const Size(1080, 2400); tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); - SharedPreferences.setMockInitialValues({}); + SharedPreferences.setMockInitialValues({ + 'topSpeedMph': 164.0, + 'fastest0To60': 2.9, + 'fastest0To100': 6.4, + 'maxLeanLeft': 48.0, + }); final prefs = await SharedPreferences.getInstance(); await tester.pumpWidget( @@ -92,15 +97,14 @@ void main() { sharedPreferencesProvider.overrideWithValue(prefs), ], child: const MaterialApp( - home: EvolutionRecordsScreen(), + home: RecordsScreen(), ), ), ); await tester.pumpAndSettle(); - expect(find.text('MOTOR SIGNAL'), findsOneWidget); - expect(find.text('PERSONAL RECORDS'), findsOneWidget); + expect(find.text('Personal Records'), findsOneWidget); expect(find.text('TOP SPEED'), findsOneWidget); expect(find.text('0-60'), findsOneWidget); expect(find.text('0-100'), findsOneWidget); @@ -108,3 +112,4 @@ void main() { }); }); } + diff --git a/test/ride_detail_responsive_test.dart b/test/ride_detail_responsive_test.dart index 55366c3..184e956 100644 --- a/test/ride_detail_responsive_test.dart +++ b/test/ride_detail_responsive_test.dart @@ -1,8 +1,9 @@ +import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:motorcycle_dash/database/database.dart'; -import 'package:motorcycle_dash/features/history/widgets/evolution_ride_details_view.dart'; +import 'package:motorcycle_dash/features/history/ride_detail_screen.dart'; import 'package:motorcycle_dash/services/github/github_sync_service.dart'; class _FakeGitHubSyncService extends ChangeNotifier implements GitHubSyncService { @@ -23,6 +24,16 @@ class _FakeGitHubSyncService extends ChangeNotifier implements GitHubSyncService void main() { TestWidgetsFlutterBinding.ensureInitialized(); + late AppDatabase db; + + setUp(() { + db = AppDatabase.forTesting(NativeDatabase.memory()); + }); + + tearDown(() async { + await db.close(); + }); + Ride makeTestRide() { return Ride( id: 1, @@ -54,12 +65,12 @@ void main() { await tester.pumpWidget( ProviderScope( overrides: [ + databaseProvider.overrideWithValue(db), githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()), ], child: MaterialApp( - home: EvolutionRideDetailsView( + home: RideDetailScreen( ride: ride, - events: const [], ), ), ), @@ -82,12 +93,41 @@ void main() { await tester.pumpWidget( ProviderScope( overrides: [ + databaseProvider.overrideWithValue(db), githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()), ], child: MaterialApp( - home: EvolutionRideDetailsView( + home: RideDetailScreen( + ride: ride, + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + expect(find.text('REPLAY RIDE'), findsOneWidget); + expect(find.text('EXPORT GPX'), findsOneWidget); + expect(find.text('EXPORT CSV'), findsOneWidget); + }); + + testWidgets('renders on landscape 800x360 screen without overflow', (tester) async { + tester.view.physicalSize = const Size(800, 360); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + final ride = makeTestRide(); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + databaseProvider.overrideWithValue(db), + githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()), + ], + child: MaterialApp( + home: RideDetailScreen( ride: ride, - events: const [], ), ), ), @@ -101,3 +141,4 @@ void main() { expect(find.text('EXPORT CSV'), findsOneWidget); }); } +