feat: unify history records and garage experience

This commit is contained in:
Developer 2026-09-05 03:17:11 -04:00
parent 9a31819952
commit d9502487d1
12 changed files with 2665 additions and 2418 deletions

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../models/motorcycle_models.dart'; import '../../models/motorcycle_models.dart';
import '../../core/theme/theme_extensions.dart';
import '../../services/entitlements/entitlement_providers.dart'; import '../../services/entitlements/entitlement_providers.dart';
import '../../services/entitlements/feature_access_service.dart'; import '../../services/entitlements/feature_access_service.dart';
import '../entitlements/widgets/pro_lock_badge.dart'; import '../entitlements/widgets/pro_lock_badge.dart';
@ -194,6 +195,8 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = context.motorColors;
return PopScope( return PopScope(
canPop: _step == 1, canPop: _step == 1,
onPopInvokedWithResult: (didPop, result) { onPopInvokedWithResult: (didPop, result) {
@ -201,7 +204,10 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
}, },
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(_getStepTitle()), title: Text(
_getStepTitle(),
style: const TextStyle(fontWeight: FontWeight.bold),
),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
onPressed: _goBack, onPressed: _goBack,
@ -213,10 +219,10 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
children: [ children: [
LinearProgressIndicator( LinearProgressIndicator(
value: _step / 5.0, value: _step / 5.0,
backgroundColor: Colors.white10, backgroundColor: colors.outlineVariant,
valueColor: const AlwaysStoppedAnimation(Colors.cyanAccent), valueColor: AlwaysStoppedAnimation(colors.cyberCyan),
), ),
Expanded(child: _buildStepContent()), Expanded(child: _buildStepContent(colors)),
], ],
), ),
), ),
@ -240,24 +246,25 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
} }
} }
Widget _buildStepContent() { Widget _buildStepContent(MotorSignalThemeColors colors) {
switch (_step) { switch (_step) {
case 1: case 1:
return _buildYearStep(); return _buildYearStep(colors);
case 2: case 2:
return _buildMakeStep(); return _buildMakeStep(colors);
case 3: case 3:
return _buildModelStep(); return _buildModelStep(colors);
case 4: case 4:
return _buildVariantStep(); return _buildVariantStep(colors);
case 5: case 5:
return _buildConfirmStep(); return _buildConfirmStep(colors);
default: default:
return const SizedBox(); return const SizedBox.shrink();
} }
} }
Widget _buildYearStep() { Widget _buildYearStep(MotorSignalThemeColors colors) {
final theme = Theme.of(context);
return GridView.builder( return GridView.builder(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
@ -269,22 +276,25 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
itemCount: _years.length, itemCount: _years.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final year = _years[index].year; final year = _years[index].year;
final isSelected = _selectedYear == year;
return OutlinedButton( return OutlinedButton(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
side: BorderSide( side: BorderSide(
color: _selectedYear == year ? Colors.cyanAccent : Colors.white24, color: isSelected ? colors.cyberCyan : colors.outlineVariant,
width: isSelected ? 2 : 1,
), ),
backgroundColor: _selectedYear == year backgroundColor: isSelected
? Colors.cyanAccent.withValues(alpha: 0.2) ? colors.cyberCyan.withValues(alpha: 0.2)
: null, : colors.surfaceContainer,
), ),
onPressed: () => _selectYear(year), onPressed: () => _selectYear(year),
child: Text( child: Text(
'$year', '$year',
style: const TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 16, fontSize: 16,
color: Colors.white, fontFamily: 'JetBrains Mono',
color: isSelected ? colors.cyberCyan : theme.colorScheme.onSurface,
), ),
), ),
); );
@ -292,11 +302,13 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
); );
} }
Widget _buildMakeStep() { Widget _buildMakeStep(MotorSignalThemeColors colors) {
final query = _searchController.text.toLowerCase(); final query = _searchController.text.toLowerCase();
final filtered = _makes final filtered = _makes
.where((m) => m.name.toLowerCase().contains(query)) .where((m) => m.name.toLowerCase().contains(query))
.toList(); .toList();
final theme = Theme.of(context);
return Column( return Column(
children: [ children: [
Padding( Padding(
@ -305,12 +317,12 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
controller: _searchController, controller: _searchController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Search Manufacturer...', hintText: 'Search Manufacturer...',
prefixIcon: const Icon(Icons.search, color: Colors.cyanAccent), prefixIcon: Icon(Icons.search, color: colors.cyberCyan),
filled: true, filled: true,
fillColor: Colors.white10, fillColor: colors.surfaceContainer,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none, borderSide: BorderSide(color: colors.outlineVariant),
), ),
), ),
onChanged: (_) => setState(() {}), onChanged: (_) => setState(() {}),
@ -321,20 +333,21 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: filtered.length, itemCount: filtered.length,
separatorBuilder: (_, _) => separatorBuilder: (_, _) =>
const Divider(height: 1, color: Colors.white10), Divider(height: 1, color: colors.outlineVariant),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final make = filtered[index]; final make = filtered[index];
return ListTile( return ListTile(
title: Text( title: Text(
make.name, make.name,
style: const TextStyle( style: TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 18, fontSize: 17,
color: theme.colorScheme.onSurface,
), ),
), ),
trailing: const Icon( trailing: Icon(
Icons.chevron_right, Icons.chevron_right,
color: Colors.white30, color: theme.colorScheme.onSurface.withValues(alpha: 0.3),
), ),
onTap: () => _selectMake(make), onTap: () => _selectMake(make),
); );
@ -345,11 +358,13 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
); );
} }
Widget _buildModelStep() { Widget _buildModelStep(MotorSignalThemeColors colors) {
final query = _searchController.text.toLowerCase(); final query = _searchController.text.toLowerCase();
final filtered = _models final filtered = _models
.where((m) => m.name.toLowerCase().contains(query)) .where((m) => m.name.toLowerCase().contains(query))
.toList(); .toList();
final theme = Theme.of(context);
return Column( return Column(
children: [ children: [
Padding( Padding(
@ -358,12 +373,12 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
controller: _searchController, controller: _searchController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Search Model...', hintText: 'Search Model...',
prefixIcon: const Icon(Icons.search, color: Colors.cyanAccent), prefixIcon: Icon(Icons.search, color: colors.cyberCyan),
filled: true, filled: true,
fillColor: Colors.white10, fillColor: colors.surfaceContainer,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none, borderSide: BorderSide(color: colors.outlineVariant),
), ),
), ),
onChanged: (_) => setState(() {}), onChanged: (_) => setState(() {}),
@ -374,21 +389,27 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: filtered.length, itemCount: filtered.length,
separatorBuilder: (_, _) => separatorBuilder: (_, _) =>
const Divider(height: 1, color: Colors.white10), Divider(height: 1, color: colors.outlineVariant),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final model = filtered[index]; final model = filtered[index];
return ListTile( return ListTile(
title: Text( title: Text(
model.name, model.name,
style: const TextStyle( style: TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 18, fontSize: 17,
color: theme.colorScheme.onSurface,
), ),
), ),
subtitle: Text('$_selectedYear ${_selectedMake?.name}'), subtitle: Text(
trailing: const Icon( '$_selectedYear ${_selectedMake?.name}',
style: TextStyle(
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
),
),
trailing: Icon(
Icons.chevron_right, Icons.chevron_right,
color: Colors.white30, color: theme.colorScheme.onSurface.withValues(alpha: 0.3),
), ),
onTap: () => _selectModel(model), onTap: () => _selectModel(model),
); );
@ -399,7 +420,8 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
); );
} }
Widget _buildVariantStep() { Widget _buildVariantStep(MotorSignalThemeColors colors) {
final theme = Theme.of(context);
return ListView.separated( return ListView.separated(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
itemCount: _variants.length, itemCount: _variants.length,
@ -407,14 +429,23 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final variant = _variants[index]; final variant = _variants[index];
return Card( return Card(
color: colors.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(color: colors.outlineVariant),
),
child: ListTile( child: ListTile(
title: Text( title: Text(
variant.name, 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, Icons.check_circle_outline,
color: Colors.cyanAccent, color: colors.cyberCyan,
), ),
onTap: () => _selectVariant(variant), onTap: () => _selectVariant(variant),
), ),
@ -423,19 +454,21 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
); );
} }
Widget _buildConfirmStep() { Widget _buildConfirmStep(MotorSignalThemeColors colors) {
final spec = _previewSpec; final spec = _previewSpec;
final theme = Theme.of(context);
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.all(24.0), padding: const EdgeInsets.all(20.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white10, color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.cyanAccent, width: 2), border: Border.all(color: colors.cyberCyan, width: 2),
), ),
child: Column( child: Column(
children: [ children: [
@ -443,16 +476,18 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
year: _selectedYear!, year: _selectedYear!,
make: _selectedMake!.name, make: _selectedMake!.name,
model: _selectedModel!.name, model: _selectedModel!.name,
height: 220, height: 200,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
Text( Text(
'$_selectedYear ${_selectedMake?.name} ${_selectedModel?.name}', '$_selectedYear ${_selectedMake?.name} ${_selectedModel?.name}',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: TextStyle(
fontSize: 24, fontFamily: 'Sora',
fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
), ),
), ),
if (_selectedVariant?.name != null && if (_selectedVariant?.name != null &&
@ -460,23 +495,28 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
_selectedVariant!.name, _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) ...[ if (spec != null) ...[
const Text( Text(
'SPECIFICATION SUMMARY', 'SPECIFICATION SUMMARY',
style: TextStyle( style: TextStyle(
color: Colors.cyanAccent, fontFamily: 'Sora',
color: colors.cyberCyan,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
letterSpacing: 1.2, letterSpacing: 1.0,
fontSize: 13,
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 10),
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -485,20 +525,24 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
spec.displacementCc != null spec.displacementCc != null
? '${spec.displacementCc!.toStringAsFixed(0)} cc' ? '${spec.displacementCc!.toStringAsFixed(0)} cc'
: '—', : '—',
colors,
theme,
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 10),
Expanded( Expanded(
child: _buildSpecChip( child: _buildSpecChip(
'HORSEPOWER', 'HORSEPOWER',
spec.horsepower != null spec.horsepower != null
? '${spec.horsepower!.toStringAsFixed(0)} HP' ? '${spec.horsepower!.toStringAsFixed(0)} HP'
: '—', : '—',
colors,
theme,
), ),
), ),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 10),
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -507,36 +551,43 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
spec.torqueLbFt != null spec.torqueLbFt != null
? '${spec.torqueLbFt!.toStringAsFixed(1)} lb-ft' ? '${spec.torqueLbFt!.toStringAsFixed(1)} lb-ft'
: '—', : '—',
colors,
theme,
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 10),
Expanded( Expanded(
child: _buildSpecChip( child: _buildSpecChip(
'WET WEIGHT', 'WET WEIGHT',
spec.wetWeightLbs != null spec.wetWeightLbs != null
? '${spec.wetWeightLbs!.toStringAsFixed(0)} lbs' ? '${spec.wetWeightLbs!.toStringAsFixed(0)} lbs'
: '—', : '—',
colors,
theme,
), ),
), ),
], ],
), ),
const SizedBox(height: 24), const SizedBox(height: 20),
], ],
TextField( TextField(
controller: _nicknameController, controller: _nicknameController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Nickname (Optional)', labelText: 'Nickname (Optional)',
hintText: 'e.g., Blue Beast, The Track Bike', hintText: 'e.g., Blue Beast, The Track Bike',
filled: true,
fillColor: colors.surfaceContainer,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), 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( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.cyanAccent, backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black, foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@ -547,9 +598,10 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
child: const Text( child: const Text(
'ADD TO GARAGE', 'ADD TO GARAGE',
style: TextStyle( style: TextStyle(
fontSize: 18, fontFamily: 'Sora',
fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
letterSpacing: 1.5, letterSpacing: 1.2,
), ),
), ),
), ),
@ -558,32 +610,39 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
); );
} }
Widget _buildSpecChip(String label, String value) { Widget _buildSpecChip(
String label,
String value,
MotorSignalThemeColors colors,
ThemeData theme,
) {
return Container( return Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05), color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white12), border: Border.all(color: colors.outlineVariant),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
label, label,
style: const TextStyle( style: TextStyle(
fontSize: 10, fontSize: 9.5,
color: Colors.white54, color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
letterSpacing: 1.0, letterSpacing: 0.8,
fontWeight: FontWeight.w700,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
value, value,
style: const TextStyle( style: TextStyle(
fontSize: 18, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, fontFamily: 'JetBrains Mono',
color: theme.colorScheme.onSurface,
), ),
), ),
], ],
@ -591,3 +650,4 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
); );
} }
} }

View File

@ -4,7 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../database/database.dart'; import '../../database/database.dart';
import '../../models/motorcycle_models.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 'garage_providers.dart';
import 'widgets/motorcycle_photo.dart'; import 'widgets/motorcycle_photo.dart';
import '../../services/entitlements/entitlement_providers.dart'; import '../../services/entitlements/entitlement_providers.dart';
@ -17,32 +18,20 @@ class GarageScreen extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final bikesAsync = ref.watch(garageMotorcyclesStreamProvider); final bikesAsync = ref.watch(garageMotorcyclesStreamProvider);
final themeMode = ref.watch(dashboardThemeModeProvider); final colors = context.motorColors;
return Scaffold( return Scaffold(
backgroundColor: themeMode == DashboardThemeMode.evolution
? Colors.black
: null,
appBar: AppBar( appBar: AppBar(
backgroundColor: themeMode == DashboardThemeMode.evolution title: const Text(
? Colors.transparent
: null,
elevation: 0,
title: Text(
'My Garage', 'My Garage',
style: TextStyle( style: TextStyle(fontWeight: FontWeight.bold),
fontFamily: themeMode == DashboardThemeMode.evolution
? 'Sora'
: null,
fontWeight: FontWeight.bold,
),
), ),
actions: [ actions: [
IconButton( IconButton(
key: const ValueKey('garage_add_motorcycle_appbar_button'), key: const ValueKey('garage_add_motorcycle_appbar_button'),
icon: const Icon( icon: Icon(
Icons.add_circle_outline, Icons.add_circle_outline,
color: Color(0xFF00E5FF), color: colors.cyberCyan,
), ),
tooltip: 'Add Motorcycle', tooltip: 'Add Motorcycle',
onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value), onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value),
@ -51,34 +40,50 @@ class GarageScreen extends ConsumerWidget {
), ),
body: bikesAsync.when( body: bikesAsync.when(
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => error: (err, stack) => Center(child: Text('Error loading garage: $err')),
Center(child: Text('Error loading garage: $err')),
data: (bikes) { data: (bikes) {
if (bikes.isEmpty) return _buildEmptyState(context); if (bikes.isEmpty) return _buildEmptyState(context, colors);
return ListView.builder(
padding: const EdgeInsets.all(16.0), return ResponsiveLayout(
itemCount: bikes.length, builder: (context, responsive) {
itemBuilder: (context, index) { final isMultiColumn = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
return _buildMotorcycleCard(
context, if (isMultiColumn) {
ref, return GridView.builder(
bikes[index], padding: const EdgeInsets.all(16.0),
themeMode, 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: floatingActionButton: bikesAsync.value != null && bikesAsync.value!.isNotEmpty
bikesAsync.value != null && bikesAsync.value!.isNotEmpty
? FloatingActionButton.extended( ? FloatingActionButton.extended(
key: const ValueKey('garage_add_motorcycle_fab'), key: const ValueKey('garage_add_motorcycle_fab'),
backgroundColor: Colors.cyanAccent, backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black, foregroundColor: Colors.black,
icon: const Icon(Icons.two_wheeler), icon: const Icon(Icons.two_wheeler),
label: const Text( label: const Text(
'ADD MOTORCYCLE', 'ADD MOTORCYCLE',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 0.8),
), ),
onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value), onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value),
) )
@ -107,34 +112,44 @@ class GarageScreen extends ConsumerWidget {
context.push('/garage/add'); context.push('/garage/add');
} }
Widget _buildEmptyState(BuildContext context) { Widget _buildEmptyState(BuildContext context, MotorSignalThemeColors colors) {
final theme = Theme.of(context);
return Center( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(32.0), padding: const EdgeInsets.all(32.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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 SizedBox(height: 24),
const Text( Text(
'YOUR GARAGE IS EMPTY', 'YOUR GARAGE IS EMPTY',
style: TextStyle( style: TextStyle(
fontSize: 20, fontFamily: 'Sora',
fontWeight: FontWeight.bold, fontSize: 18,
letterSpacing: 1.5, fontWeight: FontWeight.w900,
letterSpacing: 1.2,
color: theme.colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
const Text( Text(
'Add your motorcycle to personalize your ride history and performance data.', 'Add your motorcycle to personalize your ride history and performance data.',
textAlign: TextAlign.center, 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), const SizedBox(height: 32),
ElevatedButton.icon( ElevatedButton.icon(
key: const ValueKey('garage_add_motorcycle_empty_button'), key: const ValueKey('garage_add_motorcycle_empty_button'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.cyanAccent, backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black, foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 24, horizontal: 24,
@ -164,8 +179,9 @@ class GarageScreen extends ConsumerWidget {
BuildContext context, BuildContext context,
WidgetRef ref, WidgetRef ref,
GarageMotorcycle bike, GarageMotorcycle bike,
DashboardThemeMode themeMode, MotorSignalThemeColors colors, {
) { bool isGrid = false,
}) {
MotorcycleSpecification? spec; MotorcycleSpecification? spec;
if (bike.specificationJson != null) { if (bike.specificationJson != null) {
try { try {
@ -178,24 +194,25 @@ class GarageScreen extends ConsumerWidget {
final displayName = bike.nickname != null && bike.nickname!.isNotEmpty final displayName = bike.nickname != null && bike.nickname!.isNotEmpty
? '${bike.nickname} (${bike.year} ${bike.makeName} ${bike.modelName})' ? '${bike.nickname} (${bike.year} ${bike.makeName} ${bike.modelName})'
: '${bike.year} ${bike.makeName} ${bike.modelName}'; : '${bike.year} ${bike.makeName} ${bike.modelName}';
final isEvolution = themeMode == DashboardThemeMode.evolution;
final theme = Theme.of(context);
return Container( return Container(
margin: const EdgeInsets.only(bottom: 16), margin: EdgeInsets.only(bottom: isGrid ? 0 : 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isEvolution ? const Color(0xFF0D0D0D) : null, color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(isEvolution ? 14 : 12), borderRadius: BorderRadius.circular(14),
border: Border.all( border: Border.all(
color: bike.isDefault color: bike.isDefault
? const Color(0xFF00E5FF) ? colors.cyberCyan
: (isEvolution ? const Color(0xFF1F1F1F) : Colors.white12), : colors.outlineVariant,
width: bike.isDefault ? 1.8 : 1.0, width: bike.isDefault ? 1.8 : 1.0,
), ),
), ),
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(isEvolution ? 14 : 12), borderRadius: BorderRadius.circular(14),
onTap: () => context.push('/garage/${bike.id}'), onTap: () => context.push('/garage/${bike.id}'),
child: Padding( child: Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
@ -206,10 +223,10 @@ class GarageScreen extends ConsumerWidget {
year: bike.year, year: bike.year,
make: bike.makeName, make: bike.makeName,
model: bike.modelName, model: bike.modelName,
height: 190, height: 150,
borderRadius: BorderRadius.circular(isEvolution ? 10 : 8), borderRadius: BorderRadius.circular(10),
), ),
const SizedBox(height: 14), const SizedBox(height: 12),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -223,10 +240,10 @@ class GarageScreen extends ConsumerWidget {
child: Text( child: Text(
displayName, displayName,
style: TextStyle( style: TextStyle(
fontFamily: isEvolution ? 'Sora' : null, fontFamily: 'Sora',
fontSize: 17, fontSize: 16,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
color: Colors.white, color: theme.colorScheme.onSurface,
), ),
), ),
), ),
@ -237,41 +254,35 @@ class GarageScreen extends ConsumerWidget {
vertical: 2, vertical: 2,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color( color: colors.cyberCyan.withValues(alpha: 0.15),
0xFF00E5FF,
).withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
border: Border.all( border: Border.all(
color: const Color(0xFF00E5FF), color: colors.cyberCyan,
), ),
), ),
child: const Text( child: Text(
'CURRENT', 'CURRENT',
style: TextStyle( style: TextStyle(
fontFamily: 'Inter', fontFamily: 'Inter',
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Color(0xFF00E5FF), color: colors.cyberCyan,
letterSpacing: 0.5, letterSpacing: 0.5,
), ),
), ),
), ),
], ],
), ),
const SizedBox(height: 6), const SizedBox(height: 4),
Text( Text(
spec != null spec != null
? '${spec.displacementCc?.toStringAsFixed(0) ?? "—"} cc • ${spec.horsepower?.toStringAsFixed(0) ?? "—"} hp' ? '${spec.displacementCc?.toStringAsFixed(0) ?? "—"} cc • ${spec.horsepower?.toStringAsFixed(0) ?? "—"} hp'
: '${bike.year} • ${bike.makeName}', : '${bike.year} • ${bike.makeName}',
style: TextStyle( style: TextStyle(
fontFamily: isEvolution ? 'JetBrains Mono' : null, fontFamily: 'JetBrains Mono',
color: isEvolution color: colors.cyberCyan,
? const Color(0xFF00E5FF) fontSize: 12.5,
: Colors.white70, fontWeight: FontWeight.w700,
fontSize: 13,
fontWeight: isEvolution
? FontWeight.w700
: FontWeight.normal,
), ),
), ),
], ],
@ -287,15 +298,18 @@ class GarageScreen extends ConsumerWidget {
runSpacing: 4, runSpacing: 4,
children: [ children: [
TextButton.icon( TextButton.icon(
icon: const Icon(Icons.directions_bike, size: 18), icon: Icon(Icons.directions_bike, size: 18, color: colors.cyberCyan),
label: const Text('View Specs & Stats'), label: Text(
'View Specs & Stats',
style: TextStyle(color: colors.cyberCyan, fontWeight: FontWeight.bold),
),
onPressed: () => context.push('/garage/${bike.id}'), onPressed: () => context.push('/garage/${bike.id}'),
), ),
if (!bike.isDefault) if (!bike.isDefault)
OutlinedButton( OutlinedButton(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: Colors.white, foregroundColor: theme.colorScheme.onSurface,
side: const BorderSide(color: Colors.white30), side: BorderSide(color: colors.outlineVariant),
), ),
onPressed: () { onPressed: () {
ref ref
@ -321,3 +335,4 @@ class GarageScreen extends ConsumerWidget {
); );
} }
} }

View File

@ -4,8 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../database/database.dart'; import '../../database/database.dart';
import '../../models/motorcycle_models.dart'; import '../../models/motorcycle_models.dart';
import '../../core/theme/evolution_theme_config.dart'; import '../../core/responsive/responsive_layout.dart';
import 'widgets/evolution_motorcycle_detail_view.dart'; import '../../core/theme/theme_extensions.dart';
import 'widgets/motorcycle_photo.dart'; import 'widgets/motorcycle_photo.dart';
import 'garage_providers.dart'; import 'garage_providers.dart';
@ -18,7 +18,8 @@ class MotorcycleDetailScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final bikesAsync = ref.watch(garageMotorcyclesStreamProvider); final bikesAsync = ref.watch(garageMotorcyclesStreamProvider);
final statsAsync = ref.watch(motorcyclePerformanceStatsProvider(motorcycleId)); final statsAsync = ref.watch(motorcyclePerformanceStatsProvider(motorcycleId));
final themeMode = ref.watch(dashboardThemeModeProvider); final colors = context.motorColors;
final theme = Theme.of(context);
return bikesAsync.when( return bikesAsync.when(
loading: () => const Scaffold(body: Center(child: CircularProgressIndicator())), loading: () => const Scaffold(body: Center(child: CircularProgressIndicator())),
@ -53,21 +54,15 @@ class MotorcycleDetailScreen extends ConsumerWidget {
} catch (_) {} } catch (_) {}
} }
if (themeMode == DashboardThemeMode.evolution) {
return EvolutionMotorcycleDetailView(
bike: bike,
spec: spec,
onEdit: () => _showEditNicknameDialog(context, ref, bike),
onDelete: () => _confirmDelete(context, ref, bike),
);
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: FittedBox( title: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft, 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: [ actions: [
IconButton( IconButton(
@ -76,87 +71,154 @@ class MotorcycleDetailScreen extends ConsumerWidget {
onPressed: () => _showEditNicknameDialog(context, ref, bike), onPressed: () => _showEditNicknameDialog(context, ref, bike),
), ),
IconButton( IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.redAccent), icon: Icon(Icons.delete_outline, color: colors.raceRed),
tooltip: 'Delete Motorcycle', tooltip: 'Delete Motorcycle',
onPressed: () => _confirmDelete(context, ref, bike), onPressed: () => _confirmDelete(context, ref, bike, colors),
), ),
], ],
), ),
body: SingleChildScrollView( body: ResponsiveLayout(
padding: const EdgeInsets.all(16.0), builder: (context, responsive) {
child: Column( final isWide = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ if (isWide) {
_buildHeaderCard(context, ref, bike, spec), return SingleChildScrollView(
const SizedBox(height: 24), padding: const EdgeInsets.all(16.0),
_buildMyPerformanceSection(context, statsAsync, spec), child: Row(
const SizedBox(height: 24), crossAxisAlignment: CrossAxisAlignment.start,
_buildSpecificationSheet(context, spec), 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( return Card(
color: const Color(0xFF161616), elevation: 2,
color: colors.surfaceContainer,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), 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( child: Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MotorcyclePhoto( MotorcyclePhoto(
year: bike.year, year: bike.year,
make: bike.makeName, make: bike.makeName,
model: bike.modelName, model: bike.modelName,
height: 210, height: 190,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
showAttribution: true, showAttribution: true,
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Row( Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Text(
child: Column( '${bike.year}',
crossAxisAlignment: CrossAxisAlignment.start, style: TextStyle(
children: [ fontSize: 14,
Text( color: colors.cyberCyan,
'${bike.year}', fontWeight: FontWeight.bold,
style: const TextStyle(fontSize: 16, color: Colors.cyanAccent, fontWeight: FontWeight.bold), fontFamily: 'JetBrains Mono',
),
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.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 SizedBox(height: 16),
const Divider(color: Colors.white10), Divider(color: colors.outlineVariant, height: 1),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
_buildHeaderStat('DISPLACEMENT', spec?.displacementCc != null ? '${spec!.displacementCc!.toStringAsFixed(0)} cc' : '—'), _buildHeaderStat(
_buildHeaderStat('POWER', spec?.horsepower != null ? '${spec!.horsepower!.toStringAsFixed(0)} HP' : '—'), 'DISPLACEMENT',
_buildHeaderStat('TORQUE', spec?.torqueLbFt != null ? '${spec!.torqueLbFt!.toStringAsFixed(1)} lb-ft' : '—'), 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), const SizedBox(height: 16),
@ -166,30 +228,46 @@ class MotorcycleDetailScreen extends ConsumerWidget {
? Container( ? Container(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.cyanAccent.withValues(alpha: 0.15), color: colors.cyberCyan.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.cyanAccent), border: Border.all(color: colors.cyberCyan),
), ),
child: const Center( child: Center(
child: Text( child: Text(
'CURRENT MOTORCYCLE', '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( : OutlinedButton(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
side: const BorderSide(color: Colors.cyanAccent), side: BorderSide(color: colors.cyberCyan),
foregroundColor: Colors.cyanAccent, foregroundColor: colors.cyberCyan,
), ),
onPressed: () { onPressed: () {
ref.read(garageControllerProvider).setDefaultMotorcycle(bike.id); ref.read(garageControllerProvider).setDefaultMotorcycle(bike.id);
ScaffoldMessenger.of(context).showSnackBar( 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) { Widget _buildHeaderStat(String label, String value, ThemeData theme) {
return Column( return Expanded(
children: [ child: Column(
Text(label, style: const TextStyle(fontSize: 10, color: Colors.white54, letterSpacing: 1.2)), children: [
const SizedBox(height: 4), FittedBox(
Text(value, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)), 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<MotorcycleStats> statsAsync, MotorcycleSpecification? spec) { Widget _buildMyPerformanceSection(
BuildContext context,
AsyncValue<MotorcycleStats> statsAsync,
MotorcycleSpecification? spec,
MotorSignalThemeColors colors,
ThemeData theme,
) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( Text(
'MY PERFORMANCE', '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), const SizedBox(height: 12),
statsAsync.when( statsAsync.when(
@ -225,9 +339,16 @@ class MotorcycleDetailScreen extends ConsumerWidget {
children: [ children: [
Row( Row(
children: [ children: [
Expanded(child: _buildPerfTile('TOTAL RIDES', '${stats.rideCount}')), Expanded(child: _buildPerfTile('TOTAL RIDES', '${stats.rideCount}', colors, theme)),
const SizedBox(width: 12), 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), const SizedBox(height: 12),
@ -237,7 +358,11 @@ class MotorcycleDetailScreen extends ConsumerWidget {
child: _buildPerfTile( child: _buildPerfTile(
'BEST TOP SPEED', 'BEST TOP SPEED',
'${stats.topSpeedMph.toStringAsFixed(1)} MPH', '${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), const SizedBox(width: 12),
@ -245,7 +370,11 @@ class MotorcycleDetailScreen extends ConsumerWidget {
child: _buildPerfTile( child: _buildPerfTile(
'BEST 0–60', 'BEST 0–60',
stats.best0to60Ms != null ? '${(stats.best0to60Ms! / 1000.0).toStringAsFixed(2)} s' : '—', 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), const SizedBox(height: 12),
Row( Row(
children: [ 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), 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( return Container(
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05), color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white10), border: Border.all(color: colors.outlineVariant, width: 1.2),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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), 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) ...[ if (published != null) ...[
const SizedBox(height: 4), 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) { if (spec == null) {
return const Card( return Card(
child: Padding( color: colors.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: colors.outlineVariant),
),
child: const Padding(
padding: EdgeInsets.all(16.0), 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( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( Text(
'SPECIFICATIONS', '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), const SizedBox(height: 12),
_buildSpecGroup('ENGINE', [ _buildSpecGroup('ENGINE', [
@ -317,23 +511,23 @@ class MotorcycleDetailScreen extends ConsumerWidget {
_SpecRow('Compression Ratio', spec.compressionRatio), _SpecRow('Compression Ratio', spec.compressionRatio),
_SpecRow('Cooling System', spec.coolingSystem), _SpecRow('Cooling System', spec.coolingSystem),
_SpecRow('Fuel System', spec.fuelSystem), _SpecRow('Fuel System', spec.fuelSystem),
]), ], colors, theme),
_buildSpecGroup('PERFORMANCE (PUBLISHED)', [ _buildSpecGroup('PERFORMANCE (PUBLISHED)', [
_SpecRow('Claimed Top Speed', spec.claimedTopSpeedMph != null ? '~${spec.claimedTopSpeedMph!.toStringAsFixed(0)} MPH' : null), _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 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), _SpecRow('Claimed 1/4 Mile', spec.claimedQuarterMileSec != null ? '~${spec.claimedQuarterMileSec!.toStringAsFixed(1)} sec' : null),
]), ], colors, theme),
_buildSpecGroup('WEIGHT & DIMENSIONS', [ _buildSpecGroup('WEIGHT & DIMENSIONS', [
_SpecRow('Wet Weight', spec.wetWeightLbs != null ? '${spec.wetWeightLbs!.toStringAsFixed(0)} lbs' : null), _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('Dry Weight', spec.dryWeightLbs != null ? '${spec.dryWeightLbs!.toStringAsFixed(0)} lbs' : null),
_SpecRow('Seat Height', spec.seatHeightInches != null ? '${spec.seatHeightInches!.toStringAsFixed(1)} in' : null), _SpecRow('Seat Height', spec.seatHeightInches != null ? '${spec.seatHeightInches!.toStringAsFixed(1)} in' : null),
_SpecRow('Wheelbase', spec.wheelbaseInches != null ? '${spec.wheelbaseInches!.toStringAsFixed(1)} in' : null), _SpecRow('Wheelbase', spec.wheelbaseInches != null ? '${spec.wheelbaseInches!.toStringAsFixed(1)} in' : null),
]), ], colors, theme),
_buildSpecGroup('FUEL', [ _buildSpecGroup('FUEL', [
_SpecRow('Tank Capacity', spec.fuelCapacityGallons != null ? '${spec.fuelCapacityGallons!.toStringAsFixed(1)} gal' : null), _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('Fuel Economy', spec.fuelEconomyMpg != null ? '${spec.fuelEconomyMpg!.toStringAsFixed(0)} MPG' : null),
_SpecRow('Estimated Range', spec.estimatedRangeMiles != null ? '~${spec.estimatedRangeMiles!.toStringAsFixed(0)} miles' : null), _SpecRow('Estimated Range', spec.estimatedRangeMiles != null ? '~${spec.estimatedRangeMiles!.toStringAsFixed(0)} miles' : null),
]), ], colors, theme),
_buildSpecGroup('CHASSIS & BRAKES', [ _buildSpecGroup('CHASSIS & BRAKES', [
_SpecRow('Frame Type', spec.frameType), _SpecRow('Frame Type', spec.frameType),
_SpecRow('Front Suspension', spec.frontSuspension), _SpecRow('Front Suspension', spec.frontSuspension),
@ -343,48 +537,84 @@ class MotorcycleDetailScreen extends ConsumerWidget {
_SpecRow('ABS System', spec.absType), _SpecRow('ABS System', spec.absType),
_SpecRow('Front Tire', spec.frontTireSize), _SpecRow('Front Tire', spec.frontTireSize),
_SpecRow('Rear Tire', spec.rearTireSize), _SpecRow('Rear Tire', spec.rearTireSize),
]), ], colors, theme),
_buildSpecGroup('TRANSMISSION', [ _buildSpecGroup('TRANSMISSION', [
_SpecRow('Transmission Type', spec.transmissionType), _SpecRow('Transmission Type', spec.transmissionType),
_SpecRow('Gears', spec.gearCount?.toString()), _SpecRow('Gears', spec.gearCount?.toString()),
_SpecRow('Final Drive', spec.finalDrive), _SpecRow('Final Drive', spec.finalDrive),
]), ], colors, theme),
if (spec.source != null) ...[ if (spec.source != null) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
'Specifications Source: ${spec.source}', '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(); 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( return Card(
elevation: 1,
color: colors.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: colors.outlineVariant),
),
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(14.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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), const SizedBox(height: 8),
...validRows.map( ...validRows.map(
(r) => Padding( (r) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0), padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(r.label, style: const TextStyle(color: Colors.white70)), Text(
Flexible( r.label,
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(width: 8),
Expanded(
child: Text( child: Text(
r.value!, r.value!,
textAlign: TextAlign.right, 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')), TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
ElevatedButton( ElevatedButton(
onPressed: () { 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); Navigator.pop(context);
}, },
child: const Text('Save'), 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( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: const Text('Delete Motorcycle?'), 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: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent), style: ElevatedButton.styleFrom(backgroundColor: colors.raceRed),
onPressed: () { onPressed: () {
ref.read(garageControllerProvider).deleteMotorcycle(bike.id); ref.read(garageControllerProvider).deleteMotorcycle(bike.id);
Navigator.pop(context); Navigator.pop(context);
@ -453,3 +693,4 @@ class _SpecRow {
final String? value; final String? value;
_SpecRow(this.label, this.value); _SpecRow(this.label, this.value);
} }

View File

@ -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<Widget> 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)),
),
],
),
);
}
}

View File

@ -1,7 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:intl/intl.dart';
import '../../database/database.dart'; import '../../database/database.dart';
import '../../core/responsive/responsive_layout.dart';
import '../../core/theme/theme_extensions.dart';
import '../garage/garage_providers.dart'; import '../garage/garage_providers.dart';
import 'ride_detail_screen.dart'; import 'ride_detail_screen.dart';
@ -13,20 +16,25 @@ class HistoryScreen extends ConsumerStatefulWidget {
} }
class _HistoryScreenState extends ConsumerState<HistoryScreen> { class _HistoryScreenState extends ConsumerState<HistoryScreen> {
int? int? _selectedMotorcycleId; // null = All Motorcycles, -1 = Current, else specific ID
_selectedMotorcycleId; // null = All Motorcycles, -1 = Current, else specific ID
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final database = ref.watch(databaseProvider); final database = ref.watch(databaseProvider);
final garageBikesAsync = ref.watch(garageMotorcyclesStreamProvider); final garageBikesAsync = ref.watch(garageMotorcyclesStreamProvider);
final defaultBike = ref.watch(defaultMotorcycleProvider); final defaultBike = ref.watch(defaultMotorcycleProvider);
final colors = context.motorColors;
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Ride History')), appBar: AppBar(
title: const Text(
'Ride History',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
body: Column( body: Column(
children: [ children: [
_buildFilterBar(garageBikesAsync.value ?? [], defaultBike), _buildFilterBar(context, garageBikesAsync.value ?? [], defaultBike),
Expanded( Expanded(
child: StreamBuilder<List<Ride>>( child: StreamBuilder<List<Ride>>(
stream: database.select(database.rides).watch(), stream: database.select(database.rides).watch(),
@ -57,148 +65,54 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
rides.sort((a, b) => b.startTime.compareTo(a.startTime)); rides.sort((a, b) => b.startTime.compareTo(a.startTime));
if (rides.isEmpty) { if (rides.isEmpty) {
return const Center( return Center(
child: Text( child: Column(
'No recorded rides matching filter.', mainAxisAlignment: MainAxisAlignment.center,
style: TextStyle(fontSize: 18, color: Colors.grey), 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( return ResponsiveLayout(
padding: const EdgeInsets.all(8), builder: (context, responsive) {
itemCount: rides.length, final isMultiColumn = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
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 Card( if (isMultiColumn) {
elevation: 4, return GridView.builder(
margin: const EdgeInsets.symmetric( padding: const EdgeInsets.all(12),
vertical: 8, gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
horizontal: 4, maxCrossAxisExtent: 600,
), mainAxisExtent: 175,
child: InkWell( crossAxisSpacing: 12,
key: ValueKey('history_ride_card_${ride.id}'), mainAxisSpacing: 12,
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',
),
],
),
],
),
), ),
), 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<HistoryScreen> {
); );
} }
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( Widget _buildFilterBar(
BuildContext context,
List<GarageMotorcycle> bikes, List<GarageMotorcycle> bikes,
GarageMotorcycle? defaultBike, GarageMotorcycle? defaultBike,
) { ) {
if (bikes.isEmpty) return const SizedBox(); if (bikes.isEmpty) return const SizedBox.shrink();
final colors = context.motorColors;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row( child: Row(
children: [ children: [
const Icon(Icons.filter_alt, color: Colors.cyanAccent, size: 20), Icon(Icons.filter_alt, color: colors.cyberCyan, size: 20),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white10, color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all(color: colors.outlineVariant),
), ),
child: DropdownButtonHideUnderline( child: DropdownButtonHideUnderline(
child: DropdownButton<int?>( child: DropdownButton<int?>(
value: _selectedMotorcycleId, value: _selectedMotorcycleId,
isExpanded: true, isExpanded: true,
dropdownColor: const Color(0xFF1E1E1E), dropdownColor: colors.surfaceContainerHigh,
hint: const Text('Filter by Motorcycle'), hint: const Text('Filter by Motorcycle'),
items: [ items: [
const DropdownMenuItem<int?>( const DropdownMenuItem<int?>(
value: null, value: null,
child: Text('All Motorcycles'), child: Text('All Motorcycles', overflow: TextOverflow.ellipsis),
), ),
if (defaultBike != null) if (defaultBike != null)
DropdownMenuItem<int?>( DropdownMenuItem<int?>(
value: -1, value: -1,
child: Text('Current (${defaultBike.modelName})'), child: Text('Current (${defaultBike.modelName})', overflow: TextOverflow.ellipsis),
), ),
...bikes.map( ...bikes.map(
(b) => DropdownMenuItem<int?>( (b) => DropdownMenuItem<int?>(
value: b.id, 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<HistoryScreen> {
); );
} }
Widget _buildStatCol(String label, String val) { Widget _buildStatCol(BuildContext context, String label, String val, {Color? valueColor}) {
final theme = Theme.of(context);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)),
Text( Text(
val, label,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), 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,
),
),
), ),
], ],
); );
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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>(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),
),
),
],
),
],
),
);
}
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/theme/evolution_theme_config.dart'; import '../../core/responsive/responsive_layout.dart';
import 'evolution_records_screen.dart'; import '../../core/theme/theme_extensions.dart';
import 'records_service.dart'; import 'records_service.dart';
class RecordsScreen extends ConsumerWidget { class RecordsScreen extends ConsumerWidget {
@ -9,113 +9,511 @@ class RecordsScreen extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(dashboardThemeModeProvider);
if (themeMode == DashboardThemeMode.evolution) {
return const EvolutionRecordsScreen();
}
final records = ref.watch(recordsServiceProvider); final records = ref.watch(recordsServiceProvider);
final colors = context.motorColors;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Personal Records'), title: const Text(
'Personal Records',
style: TextStyle(fontWeight: FontWeight.bold),
),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.emoji_events, color: Colors.amberAccent), icon: Icon(Icons.emoji_events, color: colors.warningAmber),
tooltip: 'Trophies',
onPressed: () {}, onPressed: () {},
), ),
], ],
), ),
body: ListView( body: ResponsiveLayout(
padding: const EdgeInsets.all(16), 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: [ 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( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded(child: _buildRecordItem('Max Lean (Left)', records.maxLeanLeft, '°', isTime: false)), Text(
Expanded(child: _buildRecordItem('Max Lean (Right)', records.maxLeanRight, '°', isTime: false)), '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), const SizedBox(height: 12),
Row( Center(
children: [ child: FittedBox(
Expanded(child: _buildRecordItem('Max Accel', records.maxAccelG, 'G', isTime: false)), fit: BoxFit.scaleDown,
Expanded(child: _buildRecordItem('Max Braking', records.maxBrakingG, 'G', isTime: false)), 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) { Widget _buildPerformanceTimesCard(
return Padding( BuildContext context,
padding: const EdgeInsets.symmetric(vertical: 8), PersonalRecords records,
child: Text( MotorSignalThemeColors colors,
title, ) {
style: const TextStyle( return Container(
color: Colors.cyanAccent, padding: const EdgeInsets.all(16),
fontSize: 14, decoration: BoxDecoration(
fontWeight: FontWeight.bold, color: colors.surfaceContainer,
letterSpacing: 2, 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}) { Widget _buildBenchmarkCell(
// If it's a time record and it's infinity, it means no record yet. String label,
// If it's a normal record and it's 0, it means no record yet. double value,
bool hasRecord = isTime ? value != double.infinity : value > 0; double maxScale,
MotorSignalThemeColors colors,
return Card( BuildContext context,
color: Colors.white10, ) {
margin: const EdgeInsets.only(bottom: 8, right: 8), final hasRecord = value != double.infinity && value > 0;
child: Padding( final frac = hasRecord ? (value / maxScale).clamp(0.1, 1.0) : 0.0;
padding: const EdgeInsets.all(16.0), final theme = Theme.of(context);
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, return Padding(
children: [ padding: const EdgeInsets.symmetric(horizontal: 10),
Text(label, style: const TextStyle(color: Colors.white70)), child: Column(
const SizedBox(height: 4), crossAxisAlignment: CrossAxisAlignment.start,
Row( children: [
crossAxisAlignment: CrossAxisAlignment.baseline, Text(
textBaseline: TextBaseline.alphabetic, label,
children: [ style: TextStyle(
Text( fontFamily: 'Inter',
hasRecord ? (isTime ? value.toStringAsFixed(2) : value.toStringAsFixed(1)) : '--', fontSize: 11.5,
style: const TextStyle( fontWeight: FontWeight.w700,
fontSize: 24, color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
fontWeight: FontWeight.bold, ),
color: Colors.white, ),
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), const SizedBox(width: 4),
Text( Container(
unit, width: 6,
style: const TextStyle(color: Colors.white54, fontSize: 14), height: 12,
decoration: BoxDecoration(
color: colors.cyberCyan,
borderRadius: BorderRadius.circular(2),
),
), ),
], ],
],
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: frac,
backgroundColor: colors.outlineVariant,
valueColor: AlwaysStoppedAnimation<Color>(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,
),
),
),
],
), ),
); );
} }
} }

View File

@ -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<int> 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<List<MotorcycleYear>> getYears() async => [
MotorcycleYear(2024),
MotorcycleYear(2023),
];
@override
Future<List<MotorcycleMake>> getMakes(int year) async => [
MotorcycleMake(id: 'yamaha', name: 'Yamaha'),
MotorcycleMake(id: 'ducati', name: 'Ducati'),
];
@override
Future<List<MotorcycleModel>> getModels(int year, String makeId) async => [
MotorcycleModel(id: 'r1', name: 'YZF-R1', makeId: makeId),
MotorcycleModel(id: 'mt09', name: 'MT-09', makeId: makeId),
];
@override
Future<List<MotorcycleVariant>> getVariants(int year, String makeId, String modelId) async => [
MotorcycleVariant(id: 'std', name: 'Standard'),
];
@override
Future<MotorcycleSpecification?> 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 = <String, Size>{
'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));
});
}
});
}

View File

@ -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/core/theme/evolution_theme_config.dart';
import 'package:motorcycle_dash/features/records/records_service.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/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'; import 'package:motorcycle_dash/features/rides/ride_state_machine.dart';
void main() { void main() {
@ -78,12 +78,17 @@ void main() {
expect(find.text('START SESSION'), findsOneWidget); 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.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 1.0; tester.view.devicePixelRatio = 1.0;
addTearDown(() => tester.view.resetPhysicalSize()); 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(); final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget( await tester.pumpWidget(
@ -92,15 +97,14 @@ void main() {
sharedPreferencesProvider.overrideWithValue(prefs), sharedPreferencesProvider.overrideWithValue(prefs),
], ],
child: const MaterialApp( child: const MaterialApp(
home: EvolutionRecordsScreen(), home: RecordsScreen(),
), ),
), ),
); );
await tester.pumpAndSettle(); 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('TOP SPEED'), findsOneWidget);
expect(find.text('0-60'), findsOneWidget); expect(find.text('0-60'), findsOneWidget);
expect(find.text('0-100'), findsOneWidget); expect(find.text('0-100'), findsOneWidget);
@ -108,3 +112,4 @@ void main() {
}); });
}); });
} }

View File

@ -1,8 +1,9 @@
import 'package:drift/native.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:motorcycle_dash/database/database.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'; import 'package:motorcycle_dash/services/github/github_sync_service.dart';
class _FakeGitHubSyncService extends ChangeNotifier implements GitHubSyncService { class _FakeGitHubSyncService extends ChangeNotifier implements GitHubSyncService {
@ -23,6 +24,16 @@ class _FakeGitHubSyncService extends ChangeNotifier implements GitHubSyncService
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
late AppDatabase db;
setUp(() {
db = AppDatabase.forTesting(NativeDatabase.memory());
});
tearDown(() async {
await db.close();
});
Ride makeTestRide() { Ride makeTestRide() {
return Ride( return Ride(
id: 1, id: 1,
@ -54,12 +65,12 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
ProviderScope( ProviderScope(
overrides: [ overrides: [
databaseProvider.overrideWithValue(db),
githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()), githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()),
], ],
child: MaterialApp( child: MaterialApp(
home: EvolutionRideDetailsView( home: RideDetailScreen(
ride: ride, ride: ride,
events: const [],
), ),
), ),
), ),
@ -82,12 +93,41 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
ProviderScope( ProviderScope(
overrides: [ overrides: [
databaseProvider.overrideWithValue(db),
githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()), githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()),
], ],
child: MaterialApp( 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, ride: ride,
events: const [],
), ),
), ),
), ),
@ -101,3 +141,4 @@ void main() {
expect(find.text('EXPORT CSV'), findsOneWidget); expect(find.text('EXPORT CSV'), findsOneWidget);
}); });
} }