feat: unify history records and garage experience
This commit is contained in:
parent
9a31819952
commit
d9502487d1
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../models/motorcycle_models.dart';
|
||||
import '../../core/theme/theme_extensions.dart';
|
||||
import '../../services/entitlements/entitlement_providers.dart';
|
||||
import '../../services/entitlements/feature_access_service.dart';
|
||||
import '../entitlements/widgets/pro_lock_badge.dart';
|
||||
|
|
@ -194,6 +195,8 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = context.motorColors;
|
||||
|
||||
return PopScope(
|
||||
canPop: _step == 1,
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
|
|
@ -201,7 +204,10 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_getStepTitle()),
|
||||
title: Text(
|
||||
_getStepTitle(),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: _goBack,
|
||||
|
|
@ -213,10 +219,10 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
children: [
|
||||
LinearProgressIndicator(
|
||||
value: _step / 5.0,
|
||||
backgroundColor: Colors.white10,
|
||||
valueColor: const AlwaysStoppedAnimation(Colors.cyanAccent),
|
||||
backgroundColor: colors.outlineVariant,
|
||||
valueColor: AlwaysStoppedAnimation(colors.cyberCyan),
|
||||
),
|
||||
Expanded(child: _buildStepContent()),
|
||||
Expanded(child: _buildStepContent(colors)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -240,24 +246,25 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
Widget _buildStepContent() {
|
||||
Widget _buildStepContent(MotorSignalThemeColors colors) {
|
||||
switch (_step) {
|
||||
case 1:
|
||||
return _buildYearStep();
|
||||
return _buildYearStep(colors);
|
||||
case 2:
|
||||
return _buildMakeStep();
|
||||
return _buildMakeStep(colors);
|
||||
case 3:
|
||||
return _buildModelStep();
|
||||
return _buildModelStep(colors);
|
||||
case 4:
|
||||
return _buildVariantStep();
|
||||
return _buildVariantStep(colors);
|
||||
case 5:
|
||||
return _buildConfirmStep();
|
||||
return _buildConfirmStep(colors);
|
||||
default:
|
||||
return const SizedBox();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildYearStep() {
|
||||
Widget _buildYearStep(MotorSignalThemeColors colors) {
|
||||
final theme = Theme.of(context);
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
|
|
@ -269,22 +276,25 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
itemCount: _years.length,
|
||||
itemBuilder: (context, index) {
|
||||
final year = _years[index].year;
|
||||
final isSelected = _selectedYear == year;
|
||||
return OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(
|
||||
color: _selectedYear == year ? Colors.cyanAccent : Colors.white24,
|
||||
color: isSelected ? colors.cyberCyan : colors.outlineVariant,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
backgroundColor: _selectedYear == year
|
||||
? Colors.cyanAccent.withValues(alpha: 0.2)
|
||||
: null,
|
||||
backgroundColor: isSelected
|
||||
? colors.cyberCyan.withValues(alpha: 0.2)
|
||||
: colors.surfaceContainer,
|
||||
),
|
||||
onPressed: () => _selectYear(year),
|
||||
child: Text(
|
||||
'$year',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
color: isSelected ? colors.cyberCyan : theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -292,11 +302,13 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildMakeStep() {
|
||||
Widget _buildMakeStep(MotorSignalThemeColors colors) {
|
||||
final query = _searchController.text.toLowerCase();
|
||||
final filtered = _makes
|
||||
.where((m) => m.name.toLowerCase().contains(query))
|
||||
.toList();
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
|
|
@ -305,12 +317,12 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search Manufacturer...',
|
||||
prefixIcon: const Icon(Icons.search, color: Colors.cyanAccent),
|
||||
prefixIcon: Icon(Icons.search, color: colors.cyberCyan),
|
||||
filled: true,
|
||||
fillColor: Colors.white10,
|
||||
fillColor: colors.surfaceContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
borderSide: BorderSide(color: colors.outlineVariant),
|
||||
),
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
|
|
@ -321,20 +333,21 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: filtered.length,
|
||||
separatorBuilder: (_, _) =>
|
||||
const Divider(height: 1, color: Colors.white10),
|
||||
Divider(height: 1, color: colors.outlineVariant),
|
||||
itemBuilder: (context, index) {
|
||||
final make = filtered[index];
|
||||
return ListTile(
|
||||
title: Text(
|
||||
make.name,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
fontSize: 17,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
trailing: const Icon(
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Colors.white30,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
onTap: () => _selectMake(make),
|
||||
);
|
||||
|
|
@ -345,11 +358,13 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildModelStep() {
|
||||
Widget _buildModelStep(MotorSignalThemeColors colors) {
|
||||
final query = _searchController.text.toLowerCase();
|
||||
final filtered = _models
|
||||
.where((m) => m.name.toLowerCase().contains(query))
|
||||
.toList();
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
|
|
@ -358,12 +373,12 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search Model...',
|
||||
prefixIcon: const Icon(Icons.search, color: Colors.cyanAccent),
|
||||
prefixIcon: Icon(Icons.search, color: colors.cyberCyan),
|
||||
filled: true,
|
||||
fillColor: Colors.white10,
|
||||
fillColor: colors.surfaceContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
borderSide: BorderSide(color: colors.outlineVariant),
|
||||
),
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
|
|
@ -374,21 +389,27 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: filtered.length,
|
||||
separatorBuilder: (_, _) =>
|
||||
const Divider(height: 1, color: Colors.white10),
|
||||
Divider(height: 1, color: colors.outlineVariant),
|
||||
itemBuilder: (context, index) {
|
||||
final model = filtered[index];
|
||||
return ListTile(
|
||||
title: Text(
|
||||
model.name,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
fontSize: 17,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: Text('$_selectedYear ${_selectedMake?.name}'),
|
||||
trailing: const Icon(
|
||||
subtitle: Text(
|
||||
'$_selectedYear ${_selectedMake?.name}',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Colors.white30,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
onTap: () => _selectModel(model),
|
||||
);
|
||||
|
|
@ -399,7 +420,8 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildVariantStep() {
|
||||
Widget _buildVariantStep(MotorSignalThemeColors colors) {
|
||||
final theme = Theme.of(context);
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _variants.length,
|
||||
|
|
@ -407,14 +429,23 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
itemBuilder: (context, index) {
|
||||
final variant = _variants[index];
|
||||
return Card(
|
||||
color: colors.surfaceContainer,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: BorderSide(color: colors.outlineVariant),
|
||||
),
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
variant.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 17,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
trailing: const Icon(
|
||||
trailing: Icon(
|
||||
Icons.check_circle_outline,
|
||||
color: Colors.cyanAccent,
|
||||
color: colors.cyberCyan,
|
||||
),
|
||||
onTap: () => _selectVariant(variant),
|
||||
),
|
||||
|
|
@ -423,19 +454,21 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildConfirmStep() {
|
||||
Widget _buildConfirmStep(MotorSignalThemeColors colors) {
|
||||
final spec = _previewSpec;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white10,
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.cyanAccent, width: 2),
|
||||
border: Border.all(color: colors.cyberCyan, width: 2),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
|
|
@ -443,16 +476,18 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
year: _selectedYear!,
|
||||
make: _selectedMake!.name,
|
||||
model: _selectedModel!.name,
|
||||
height: 220,
|
||||
height: 200,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
'$_selectedYear ${_selectedMake?.name} ${_selectedModel?.name}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Sora',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
if (_selectedVariant?.name != null &&
|
||||
|
|
@ -460,23 +495,28 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_selectedVariant!.name,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 16),
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 20),
|
||||
if (spec != null) ...[
|
||||
const Text(
|
||||
Text(
|
||||
'SPECIFICATION SUMMARY',
|
||||
style: TextStyle(
|
||||
color: Colors.cyanAccent,
|
||||
fontFamily: 'Sora',
|
||||
color: colors.cyberCyan,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
letterSpacing: 1.0,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
|
|
@ -485,20 +525,24 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
spec.displacementCc != null
|
||||
? '${spec.displacementCc!.toStringAsFixed(0)} cc'
|
||||
: '—',
|
||||
colors,
|
||||
theme,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _buildSpecChip(
|
||||
'HORSEPOWER',
|
||||
spec.horsepower != null
|
||||
? '${spec.horsepower!.toStringAsFixed(0)} HP'
|
||||
: '—',
|
||||
colors,
|
||||
theme,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
|
|
@ -507,36 +551,43 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
spec.torqueLbFt != null
|
||||
? '${spec.torqueLbFt!.toStringAsFixed(1)} lb-ft'
|
||||
: '—',
|
||||
colors,
|
||||
theme,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _buildSpecChip(
|
||||
'WET WEIGHT',
|
||||
spec.wetWeightLbs != null
|
||||
? '${spec.wetWeightLbs!.toStringAsFixed(0)} lbs'
|
||||
: '—',
|
||||
colors,
|
||||
theme,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
TextField(
|
||||
controller: _nicknameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nickname (Optional)',
|
||||
hintText: 'e.g., Blue Beast, The Track Bike',
|
||||
filled: true,
|
||||
fillColor: colors.surfaceContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: colors.outlineVariant),
|
||||
),
|
||||
prefixIcon: const Icon(Icons.edit, color: Colors.cyanAccent),
|
||||
prefixIcon: Icon(Icons.edit, color: colors.cyberCyan),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.cyanAccent,
|
||||
backgroundColor: colors.cyberCyan,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
|
|
@ -547,9 +598,10 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
child: const Text(
|
||||
'ADD TO GARAGE',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontFamily: 'Sora',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.5,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -558,32 +610,39 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildSpecChip(String label, String value) {
|
||||
Widget _buildSpecChip(
|
||||
String label,
|
||||
String value,
|
||||
MotorSignalThemeColors colors,
|
||||
ThemeData theme,
|
||||
) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.05),
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.white12),
|
||||
border: Border.all(color: colors.outlineVariant),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white54,
|
||||
letterSpacing: 1.0,
|
||||
style: TextStyle(
|
||||
fontSize: 9.5,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
letterSpacing: 0.8,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -591,3 +650,4 @@ class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
import '../../database/database.dart';
|
||||
import '../../models/motorcycle_models.dart';
|
||||
import '../../core/theme/evolution_theme_config.dart';
|
||||
import '../../core/responsive/responsive_layout.dart';
|
||||
import '../../core/theme/theme_extensions.dart';
|
||||
import 'garage_providers.dart';
|
||||
import 'widgets/motorcycle_photo.dart';
|
||||
import '../../services/entitlements/entitlement_providers.dart';
|
||||
|
|
@ -17,32 +18,20 @@ class GarageScreen extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final bikesAsync = ref.watch(garageMotorcyclesStreamProvider);
|
||||
final themeMode = ref.watch(dashboardThemeModeProvider);
|
||||
final colors = context.motorColors;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: themeMode == DashboardThemeMode.evolution
|
||||
? Colors.black
|
||||
: null,
|
||||
appBar: AppBar(
|
||||
backgroundColor: themeMode == DashboardThemeMode.evolution
|
||||
? Colors.transparent
|
||||
: null,
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
title: const Text(
|
||||
'My Garage',
|
||||
style: TextStyle(
|
||||
fontFamily: themeMode == DashboardThemeMode.evolution
|
||||
? 'Sora'
|
||||
: null,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
key: const ValueKey('garage_add_motorcycle_appbar_button'),
|
||||
icon: const Icon(
|
||||
icon: Icon(
|
||||
Icons.add_circle_outline,
|
||||
color: Color(0xFF00E5FF),
|
||||
color: colors.cyberCyan,
|
||||
),
|
||||
tooltip: 'Add Motorcycle',
|
||||
onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value),
|
||||
|
|
@ -51,34 +40,50 @@ class GarageScreen extends ConsumerWidget {
|
|||
),
|
||||
body: bikesAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) =>
|
||||
Center(child: Text('Error loading garage: $err')),
|
||||
error: (err, stack) => Center(child: Text('Error loading garage: $err')),
|
||||
data: (bikes) {
|
||||
if (bikes.isEmpty) return _buildEmptyState(context);
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
itemCount: bikes.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildMotorcycleCard(
|
||||
context,
|
||||
ref,
|
||||
bikes[index],
|
||||
themeMode,
|
||||
if (bikes.isEmpty) return _buildEmptyState(context, colors);
|
||||
|
||||
return ResponsiveLayout(
|
||||
builder: (context, responsive) {
|
||||
final isMultiColumn = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
|
||||
|
||||
if (isMultiColumn) {
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 550,
|
||||
mainAxisExtent: 420,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: bikes.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildMotorcycleCard(context, ref, bikes[index], colors, isGrid: true);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
itemCount: bikes.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildMotorcycleCard(context, ref, bikes[index], colors, isGrid: false);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton:
|
||||
bikesAsync.value != null && bikesAsync.value!.isNotEmpty
|
||||
floatingActionButton: bikesAsync.value != null && bikesAsync.value!.isNotEmpty
|
||||
? FloatingActionButton.extended(
|
||||
key: const ValueKey('garage_add_motorcycle_fab'),
|
||||
backgroundColor: Colors.cyanAccent,
|
||||
backgroundColor: colors.cyberCyan,
|
||||
foregroundColor: Colors.black,
|
||||
icon: const Icon(Icons.two_wheeler),
|
||||
label: const Text(
|
||||
'ADD MOTORCYCLE',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 0.8),
|
||||
),
|
||||
onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value),
|
||||
)
|
||||
|
|
@ -107,34 +112,44 @@ class GarageScreen extends ConsumerWidget {
|
|||
context.push('/garage/add');
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
Widget _buildEmptyState(BuildContext context, MotorSignalThemeColors colors) {
|
||||
final theme = Theme.of(context);
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.two_wheeler, size: 80, color: Colors.white24),
|
||||
Icon(
|
||||
Icons.two_wheeler,
|
||||
size: 80,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
Text(
|
||||
'YOUR GARAGE IS EMPTY',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.5,
|
||||
fontFamily: 'Sora',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 1.2,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
Text(
|
||||
'Add your motorcycle to personalize your ride history and performance data.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.white70, height: 1.4),
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton.icon(
|
||||
key: const ValueKey('garage_add_motorcycle_empty_button'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.cyanAccent,
|
||||
backgroundColor: colors.cyberCyan,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
|
|
@ -164,8 +179,9 @@ class GarageScreen extends ConsumerWidget {
|
|||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
GarageMotorcycle bike,
|
||||
DashboardThemeMode themeMode,
|
||||
) {
|
||||
MotorSignalThemeColors colors, {
|
||||
bool isGrid = false,
|
||||
}) {
|
||||
MotorcycleSpecification? spec;
|
||||
if (bike.specificationJson != null) {
|
||||
try {
|
||||
|
|
@ -178,24 +194,25 @@ class GarageScreen extends ConsumerWidget {
|
|||
final displayName = bike.nickname != null && bike.nickname!.isNotEmpty
|
||||
? '${bike.nickname} (${bike.year} ${bike.makeName} ${bike.modelName})'
|
||||
: '${bike.year} ${bike.makeName} ${bike.modelName}';
|
||||
final isEvolution = themeMode == DashboardThemeMode.evolution;
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
margin: EdgeInsets.only(bottom: isGrid ? 0 : 16),
|
||||
decoration: BoxDecoration(
|
||||
color: isEvolution ? const Color(0xFF0D0D0D) : null,
|
||||
borderRadius: BorderRadius.circular(isEvolution ? 14 : 12),
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: bike.isDefault
|
||||
? const Color(0xFF00E5FF)
|
||||
: (isEvolution ? const Color(0xFF1F1F1F) : Colors.white12),
|
||||
? colors.cyberCyan
|
||||
: colors.outlineVariant,
|
||||
width: bike.isDefault ? 1.8 : 1.0,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(isEvolution ? 14 : 12),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
onTap: () => context.push('/garage/${bike.id}'),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
|
|
@ -206,10 +223,10 @@ class GarageScreen extends ConsumerWidget {
|
|||
year: bike.year,
|
||||
make: bike.makeName,
|
||||
model: bike.modelName,
|
||||
height: 190,
|
||||
borderRadius: BorderRadius.circular(isEvolution ? 10 : 8),
|
||||
height: 150,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
@ -223,10 +240,10 @@ class GarageScreen extends ConsumerWidget {
|
|||
child: Text(
|
||||
displayName,
|
||||
style: TextStyle(
|
||||
fontFamily: isEvolution ? 'Sora' : null,
|
||||
fontSize: 17,
|
||||
fontFamily: 'Sora',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -237,41 +254,35 @@ class GarageScreen extends ConsumerWidget {
|
|||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(
|
||||
0xFF00E5FF,
|
||||
).withValues(alpha: 0.15),
|
||||
color: colors.cyberCyan.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: const Color(0xFF00E5FF),
|
||||
color: colors.cyberCyan,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'CURRENT',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color(0xFF00E5FF),
|
||||
color: colors.cyberCyan,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
spec != null
|
||||
? '${spec.displacementCc?.toStringAsFixed(0) ?? "—"} cc • ${spec.horsepower?.toStringAsFixed(0) ?? "—"} hp'
|
||||
: '${bike.year} • ${bike.makeName}',
|
||||
style: TextStyle(
|
||||
fontFamily: isEvolution ? 'JetBrains Mono' : null,
|
||||
color: isEvolution
|
||||
? const Color(0xFF00E5FF)
|
||||
: Colors.white70,
|
||||
fontSize: 13,
|
||||
fontWeight: isEvolution
|
||||
? FontWeight.w700
|
||||
: FontWeight.normal,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
color: colors.cyberCyan,
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -287,15 +298,18 @@ class GarageScreen extends ConsumerWidget {
|
|||
runSpacing: 4,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.directions_bike, size: 18),
|
||||
label: const Text('View Specs & Stats'),
|
||||
icon: Icon(Icons.directions_bike, size: 18, color: colors.cyberCyan),
|
||||
label: Text(
|
||||
'View Specs & Stats',
|
||||
style: TextStyle(color: colors.cyberCyan, fontWeight: FontWeight.bold),
|
||||
),
|
||||
onPressed: () => context.push('/garage/${bike.id}'),
|
||||
),
|
||||
if (!bike.isDefault)
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
side: const BorderSide(color: Colors.white30),
|
||||
foregroundColor: theme.colorScheme.onSurface,
|
||||
side: BorderSide(color: colors.outlineVariant),
|
||||
),
|
||||
onPressed: () {
|
||||
ref
|
||||
|
|
@ -321,3 +335,4 @@ class GarageScreen extends ConsumerWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
import '../../database/database.dart';
|
||||
import '../../models/motorcycle_models.dart';
|
||||
import '../../core/theme/evolution_theme_config.dart';
|
||||
import 'widgets/evolution_motorcycle_detail_view.dart';
|
||||
import '../../core/responsive/responsive_layout.dart';
|
||||
import '../../core/theme/theme_extensions.dart';
|
||||
import 'widgets/motorcycle_photo.dart';
|
||||
import 'garage_providers.dart';
|
||||
|
||||
|
|
@ -18,7 +18,8 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final bikesAsync = ref.watch(garageMotorcyclesStreamProvider);
|
||||
final statsAsync = ref.watch(motorcyclePerformanceStatsProvider(motorcycleId));
|
||||
final themeMode = ref.watch(dashboardThemeModeProvider);
|
||||
final colors = context.motorColors;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return bikesAsync.when(
|
||||
loading: () => const Scaffold(body: Center(child: CircularProgressIndicator())),
|
||||
|
|
@ -53,21 +54,15 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (themeMode == DashboardThemeMode.evolution) {
|
||||
return EvolutionMotorcycleDetailView(
|
||||
bike: bike,
|
||||
spec: spec,
|
||||
onEdit: () => _showEditNicknameDialog(context, ref, bike),
|
||||
onDelete: () => _confirmDelete(context, ref, bike),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text('${bike.year} ${bike.makeName} ${bike.modelName}'),
|
||||
child: Text(
|
||||
'${bike.year} ${bike.makeName} ${bike.modelName}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
|
|
@ -76,87 +71,154 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
onPressed: () => _showEditNicknameDialog(context, ref, bike),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline, color: Colors.redAccent),
|
||||
icon: Icon(Icons.delete_outline, color: colors.raceRed),
|
||||
tooltip: 'Delete Motorcycle',
|
||||
onPressed: () => _confirmDelete(context, ref, bike),
|
||||
onPressed: () => _confirmDelete(context, ref, bike, colors),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildHeaderCard(context, ref, bike, spec),
|
||||
const SizedBox(height: 24),
|
||||
_buildMyPerformanceSection(context, statsAsync, spec),
|
||||
const SizedBox(height: 24),
|
||||
_buildSpecificationSheet(context, spec),
|
||||
],
|
||||
),
|
||||
body: ResponsiveLayout(
|
||||
builder: (context, responsive) {
|
||||
final isWide = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
|
||||
|
||||
if (isWide) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: _buildHeaderCard(context, ref, bike, spec, colors, theme),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
flex: 6,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildMyPerformanceSection(context, statsAsync, spec, colors, theme),
|
||||
const SizedBox(height: 20),
|
||||
_buildSpecificationSheet(context, spec, colors, theme),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildHeaderCard(context, ref, bike, spec, colors, theme),
|
||||
const SizedBox(height: 20),
|
||||
_buildMyPerformanceSection(context, statsAsync, spec, colors, theme),
|
||||
const SizedBox(height: 20),
|
||||
_buildSpecificationSheet(context, spec, colors, theme),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderCard(BuildContext context, WidgetRef ref, GarageMotorcycle bike, MotorcycleSpecification? spec) {
|
||||
Widget _buildHeaderCard(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
GarageMotorcycle bike,
|
||||
MotorcycleSpecification? spec,
|
||||
MotorSignalThemeColors colors,
|
||||
ThemeData theme,
|
||||
) {
|
||||
return Card(
|
||||
color: const Color(0xFF161616),
|
||||
elevation: 2,
|
||||
color: colors.surfaceContainer,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: bike.isDefault ? Colors.cyanAccent : Colors.white12, width: 2),
|
||||
side: BorderSide(
|
||||
color: bike.isDefault ? colors.cyberCyan : colors.outlineVariant,
|
||||
width: bike.isDefault ? 1.8 : 1.0,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MotorcyclePhoto(
|
||||
year: bike.year,
|
||||
make: bike.makeName,
|
||||
model: bike.modelName,
|
||||
height: 210,
|
||||
height: 190,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
showAttribution: true,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${bike.year}',
|
||||
style: const TextStyle(fontSize: 16, color: Colors.cyanAccent, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
'${bike.makeName} ${bike.modelName}'.toUpperCase(),
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w900, letterSpacing: 0.5),
|
||||
),
|
||||
if (bike.nickname != null && bike.nickname!.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Text(
|
||||
'"${bike.nickname}"',
|
||||
style: const TextStyle(fontSize: 16, fontStyle: FontStyle.italic, color: Colors.amberAccent),
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
'${bike.year}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colors.cyberCyan,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${bike.makeName} ${bike.modelName}'.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontFamily: 'Sora',
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
if (bike.nickname != null && bike.nickname!.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Text(
|
||||
'"${bike.nickname}"',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: colors.warningAmber,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(color: Colors.white10),
|
||||
Divider(color: colors.outlineVariant, height: 1),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildHeaderStat('DISPLACEMENT', spec?.displacementCc != null ? '${spec!.displacementCc!.toStringAsFixed(0)} cc' : '—'),
|
||||
_buildHeaderStat('POWER', spec?.horsepower != null ? '${spec!.horsepower!.toStringAsFixed(0)} HP' : '—'),
|
||||
_buildHeaderStat('TORQUE', spec?.torqueLbFt != null ? '${spec!.torqueLbFt!.toStringAsFixed(1)} lb-ft' : '—'),
|
||||
_buildHeaderStat(
|
||||
'DISPLACEMENT',
|
||||
spec?.displacementCc != null ? '${spec!.displacementCc!.toStringAsFixed(0)} cc' : '—',
|
||||
theme,
|
||||
),
|
||||
_buildHeaderStat(
|
||||
'POWER',
|
||||
spec?.horsepower != null ? '${spec!.horsepower!.toStringAsFixed(0)} HP' : '—',
|
||||
theme,
|
||||
),
|
||||
_buildHeaderStat(
|
||||
'TORQUE',
|
||||
spec?.torqueLbFt != null ? '${spec!.torqueLbFt!.toStringAsFixed(1)} lb-ft' : '—',
|
||||
theme,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
|
@ -166,30 +228,46 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
? Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.cyanAccent.withValues(alpha: 0.15),
|
||||
color: colors.cyberCyan.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.cyanAccent),
|
||||
border: Border.all(color: colors.cyberCyan),
|
||||
),
|
||||
child: const Center(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'CURRENT MOTORCYCLE',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.5, color: Colors.cyanAccent),
|
||||
style: TextStyle(
|
||||
fontFamily: 'Sora',
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
color: colors.cyberCyan,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
side: const BorderSide(color: Colors.cyanAccent),
|
||||
foregroundColor: Colors.cyanAccent,
|
||||
side: BorderSide(color: colors.cyberCyan),
|
||||
foregroundColor: colors.cyberCyan,
|
||||
),
|
||||
onPressed: () {
|
||||
ref.read(garageControllerProvider).setDefaultMotorcycle(bike.id);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Set ${bike.makeName} ${bike.modelName} as current motorcycle.')),
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Set ${bike.makeName} ${bike.modelName} as current motorcycle.',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('SET AS CURRENT MOTORCYCLE', style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.2)),
|
||||
child: const Text(
|
||||
'SET AS CURRENT MOTORCYCLE',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Sora',
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -198,23 +276,59 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderStat(String label, String value) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 10, color: Colors.white54, letterSpacing: 1.2)),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)),
|
||||
],
|
||||
Widget _buildHeaderStat(String label, String value, ThemeData theme) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 9.5,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
letterSpacing: 1.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMyPerformanceSection(BuildContext context, AsyncValue<MotorcycleStats> statsAsync, MotorcycleSpecification? spec) {
|
||||
Widget _buildMyPerformanceSection(
|
||||
BuildContext context,
|
||||
AsyncValue<MotorcycleStats> statsAsync,
|
||||
MotorcycleSpecification? spec,
|
||||
MotorSignalThemeColors colors,
|
||||
ThemeData theme,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
Text(
|
||||
'MY PERFORMANCE',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.cyanAccent, letterSpacing: 1.2),
|
||||
style: TextStyle(
|
||||
fontFamily: 'Sora',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colors.cyberCyan,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
statsAsync.when(
|
||||
|
|
@ -225,9 +339,16 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildPerfTile('TOTAL RIDES', '${stats.rideCount}')),
|
||||
Expanded(child: _buildPerfTile('TOTAL RIDES', '${stats.rideCount}', colors, theme)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildPerfTile('TOTAL DISTANCE', '${stats.totalDistanceMiles.toStringAsFixed(1)} MI')),
|
||||
Expanded(
|
||||
child: _buildPerfTile(
|
||||
'TOTAL DISTANCE',
|
||||
'${stats.totalDistanceMiles.toStringAsFixed(1)} MI',
|
||||
colors,
|
||||
theme,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
|
@ -237,7 +358,11 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
child: _buildPerfTile(
|
||||
'BEST TOP SPEED',
|
||||
'${stats.topSpeedMph.toStringAsFixed(1)} MPH',
|
||||
published: spec?.claimedTopSpeedMph != null ? '~${spec!.claimedTopSpeedMph!.toStringAsFixed(0)} MPH' : null,
|
||||
colors,
|
||||
theme,
|
||||
published: spec?.claimedTopSpeedMph != null
|
||||
? '~${spec!.claimedTopSpeedMph!.toStringAsFixed(0)} MPH'
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
|
@ -245,7 +370,11 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
child: _buildPerfTile(
|
||||
'BEST 0–60',
|
||||
stats.best0to60Ms != null ? '${(stats.best0to60Ms! / 1000.0).toStringAsFixed(2)} s' : '—',
|
||||
published: spec?.claimedZeroToSixtySec != null ? '~${spec!.claimedZeroToSixtySec!.toStringAsFixed(1)} s' : null,
|
||||
colors,
|
||||
theme,
|
||||
published: spec?.claimedZeroToSixtySec != null
|
||||
? '~${spec!.claimedZeroToSixtySec!.toStringAsFixed(1)} s'
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -253,9 +382,23 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildPerfTile('MAX LEAN', '${stats.maxLean.toStringAsFixed(1)}°')),
|
||||
Expanded(
|
||||
child: _buildPerfTile(
|
||||
'MAX LEAN',
|
||||
'${stats.maxLean.toStringAsFixed(1)}°',
|
||||
colors,
|
||||
theme,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildPerfTile('MAX ACCEL G', '${stats.maxAccelG.toStringAsFixed(2)} G')),
|
||||
Expanded(
|
||||
child: _buildPerfTile(
|
||||
'MAX ACCEL G',
|
||||
'${stats.maxAccelG.toStringAsFixed(2)} G',
|
||||
colors,
|
||||
theme,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
@ -266,35 +409,80 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildPerfTile(String label, String value, {String? published}) {
|
||||
Widget _buildPerfTile(
|
||||
String label,
|
||||
String value,
|
||||
MotorSignalThemeColors colors,
|
||||
ThemeData theme, {
|
||||
String? published,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.white10),
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: colors.outlineVariant, width: 1.2),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 10, color: Colors.white54, letterSpacing: 1.0)),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white)),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (published != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text('PUBLISHED: $published', style: const TextStyle(fontSize: 10, color: Colors.cyanAccent)),
|
||||
Text(
|
||||
'PUBLISHED: $published',
|
||||
style: TextStyle(
|
||||
fontSize: 9.5,
|
||||
color: colors.cyberCyan,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSpecificationSheet(BuildContext context, MotorcycleSpecification? spec) {
|
||||
Widget _buildSpecificationSheet(
|
||||
BuildContext context,
|
||||
MotorcycleSpecification? spec,
|
||||
MotorSignalThemeColors colors,
|
||||
ThemeData theme,
|
||||
) {
|
||||
if (spec == null) {
|
||||
return const Card(
|
||||
child: Padding(
|
||||
return Card(
|
||||
color: colors.surfaceContainer,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: colors.outlineVariant),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Text('Detailed specifications unavailable for this motorcycle.', style: TextStyle(color: Colors.white54)),
|
||||
child: Text(
|
||||
'Detailed specifications unavailable for this motorcycle.',
|
||||
style: TextStyle(color: Colors.white54),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -302,9 +490,15 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
Text(
|
||||
'SPECIFICATIONS',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.cyanAccent, letterSpacing: 1.2),
|
||||
style: TextStyle(
|
||||
fontFamily: 'Sora',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colors.cyberCyan,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildSpecGroup('ENGINE', [
|
||||
|
|
@ -317,23 +511,23 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
_SpecRow('Compression Ratio', spec.compressionRatio),
|
||||
_SpecRow('Cooling System', spec.coolingSystem),
|
||||
_SpecRow('Fuel System', spec.fuelSystem),
|
||||
]),
|
||||
], colors, theme),
|
||||
_buildSpecGroup('PERFORMANCE (PUBLISHED)', [
|
||||
_SpecRow('Claimed Top Speed', spec.claimedTopSpeedMph != null ? '~${spec.claimedTopSpeedMph!.toStringAsFixed(0)} MPH' : null),
|
||||
_SpecRow('Claimed 0–60 MPH', spec.claimedZeroToSixtySec != null ? '~${spec.claimedZeroToSixtySec!.toStringAsFixed(1)} sec' : null),
|
||||
_SpecRow('Claimed 1/4 Mile', spec.claimedQuarterMileSec != null ? '~${spec.claimedQuarterMileSec!.toStringAsFixed(1)} sec' : null),
|
||||
]),
|
||||
], colors, theme),
|
||||
_buildSpecGroup('WEIGHT & DIMENSIONS', [
|
||||
_SpecRow('Wet Weight', spec.wetWeightLbs != null ? '${spec.wetWeightLbs!.toStringAsFixed(0)} lbs' : null),
|
||||
_SpecRow('Dry Weight', spec.dryWeightLbs != null ? '${spec.dryWeightLbs!.toStringAsFixed(0)} lbs' : null),
|
||||
_SpecRow('Seat Height', spec.seatHeightInches != null ? '${spec.seatHeightInches!.toStringAsFixed(1)} in' : null),
|
||||
_SpecRow('Wheelbase', spec.wheelbaseInches != null ? '${spec.wheelbaseInches!.toStringAsFixed(1)} in' : null),
|
||||
]),
|
||||
], colors, theme),
|
||||
_buildSpecGroup('FUEL', [
|
||||
_SpecRow('Tank Capacity', spec.fuelCapacityGallons != null ? '${spec.fuelCapacityGallons!.toStringAsFixed(1)} gal' : null),
|
||||
_SpecRow('Fuel Economy', spec.fuelEconomyMpg != null ? '${spec.fuelEconomyMpg!.toStringAsFixed(0)} MPG' : null),
|
||||
_SpecRow('Estimated Range', spec.estimatedRangeMiles != null ? '~${spec.estimatedRangeMiles!.toStringAsFixed(0)} miles' : null),
|
||||
]),
|
||||
], colors, theme),
|
||||
_buildSpecGroup('CHASSIS & BRAKES', [
|
||||
_SpecRow('Frame Type', spec.frameType),
|
||||
_SpecRow('Front Suspension', spec.frontSuspension),
|
||||
|
|
@ -343,48 +537,84 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
_SpecRow('ABS System', spec.absType),
|
||||
_SpecRow('Front Tire', spec.frontTireSize),
|
||||
_SpecRow('Rear Tire', spec.rearTireSize),
|
||||
]),
|
||||
], colors, theme),
|
||||
_buildSpecGroup('TRANSMISSION', [
|
||||
_SpecRow('Transmission Type', spec.transmissionType),
|
||||
_SpecRow('Gears', spec.gearCount?.toString()),
|
||||
_SpecRow('Final Drive', spec.finalDrive),
|
||||
]),
|
||||
], colors, theme),
|
||||
if (spec.source != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Specifications Source: ${spec.source}',
|
||||
style: const TextStyle(fontSize: 12, color: Colors.white38),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
|
||||
fontFamily: 'Inter',
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSpecGroup(String title, List<_SpecRow> rows) {
|
||||
Widget _buildSpecGroup(
|
||||
String title,
|
||||
List<_SpecRow> rows,
|
||||
MotorSignalThemeColors colors,
|
||||
ThemeData theme,
|
||||
) {
|
||||
final validRows = rows.where((r) => r.value != null && r.value!.isNotEmpty).toList();
|
||||
if (validRows.isEmpty) return const SizedBox();
|
||||
if (validRows.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Card(
|
||||
elevation: 1,
|
||||
color: colors.surfaceContainer,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: colors.outlineVariant),
|
||||
),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
padding: const EdgeInsets.all(14.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.cyanAccent, fontSize: 14, letterSpacing: 1.0)),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Sora',
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colors.cyberCyan,
|
||||
fontSize: 12,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...validRows.map(
|
||||
(r) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(r.label, style: const TextStyle(color: Colors.white70)),
|
||||
Flexible(
|
||||
Text(
|
||||
r.label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
r.value!,
|
||||
textAlign: TextAlign.right,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fontSize: 12,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -412,7 +642,10 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
ref.read(garageControllerProvider).updateNickname(bike.id, controller.text.trim().isNotEmpty ? controller.text.trim() : null);
|
||||
ref.read(garageControllerProvider).updateNickname(
|
||||
bike.id,
|
||||
controller.text.trim().isNotEmpty ? controller.text.trim() : null,
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Save'),
|
||||
|
|
@ -423,17 +656,24 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
void _confirmDelete(BuildContext context, WidgetRef ref, GarageMotorcycle bike) {
|
||||
void _confirmDelete(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
GarageMotorcycle bike,
|
||||
MotorSignalThemeColors colors,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Delete Motorcycle?'),
|
||||
content: Text('Are you sure you want to remove ${bike.makeName} ${bike.modelName} from your Garage? Historical rides associated with this motorcycle will be preserved.'),
|
||||
content: Text(
|
||||
'Are you sure you want to remove ${bike.makeName} ${bike.modelName} from your Garage? Historical rides associated with this motorcycle will be preserved.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: colors.raceRed),
|
||||
onPressed: () {
|
||||
ref.read(garageControllerProvider).deleteMotorcycle(bike.id);
|
||||
Navigator.pop(context);
|
||||
|
|
@ -453,3 +693,4 @@ class _SpecRow {
|
|||
final String? value;
|
||||
_SpecRow(this.label, this.value);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../database/database.dart';
|
||||
import '../../core/responsive/responsive_layout.dart';
|
||||
import '../../core/theme/theme_extensions.dart';
|
||||
import '../garage/garage_providers.dart';
|
||||
import 'ride_detail_screen.dart';
|
||||
|
||||
|
|
@ -13,20 +16,25 @@ class HistoryScreen extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
||||
int?
|
||||
_selectedMotorcycleId; // null = All Motorcycles, -1 = Current, else specific ID
|
||||
int? _selectedMotorcycleId; // null = All Motorcycles, -1 = Current, else specific ID
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final database = ref.watch(databaseProvider);
|
||||
final garageBikesAsync = ref.watch(garageMotorcyclesStreamProvider);
|
||||
final defaultBike = ref.watch(defaultMotorcycleProvider);
|
||||
final colors = context.motorColors;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Ride History')),
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Ride History',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildFilterBar(garageBikesAsync.value ?? [], defaultBike),
|
||||
_buildFilterBar(context, garageBikesAsync.value ?? [], defaultBike),
|
||||
Expanded(
|
||||
child: StreamBuilder<List<Ride>>(
|
||||
stream: database.select(database.rides).watch(),
|
||||
|
|
@ -57,148 +65,54 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
|||
rides.sort((a, b) => b.startTime.compareTo(a.startTime));
|
||||
|
||||
if (rides.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'No recorded rides matching filter.',
|
||||
style: TextStyle(fontSize: 18, color: Colors.grey),
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.history,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No recorded rides matching filter.',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemCount: rides.length,
|
||||
itemBuilder: (context, index) {
|
||||
final ride = rides[index];
|
||||
final bikeName =
|
||||
(ride.motorcycleYear != null &&
|
||||
ride.motorcycleMake != null &&
|
||||
ride.motorcycleModel != null)
|
||||
? '${ride.motorcycleYear} ${ride.motorcycleMake} ${ride.motorcycleModel}'
|
||||
: null;
|
||||
return ResponsiveLayout(
|
||||
builder: (context, responsive) {
|
||||
final isMultiColumn = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 4,
|
||||
),
|
||||
child: InkWell(
|
||||
key: ValueKey('history_ride_card_${ride.id}'),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => RideDetailScreen(ride: ride),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Ride ${ride.id}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
if (bikeName != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.cyanAccent
|
||||
.withValues(alpha: 0.15),
|
||||
borderRadius:
|
||||
BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: Colors.cyanAccent
|
||||
.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
bikeName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.cyanAccent,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
ride.startTime.toLocal().toString().split(
|
||||
'.',
|
||||
)[0],
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildStatCol(
|
||||
'TRIP',
|
||||
'${ride.distanceMiles.toStringAsFixed(1)} mi',
|
||||
),
|
||||
_buildStatCol(
|
||||
'TIME',
|
||||
'${(ride.movingTime / 60).toStringAsFixed(0)} min',
|
||||
),
|
||||
_buildStatCol(
|
||||
'TOP SPD',
|
||||
'${ride.topSpeedMph.toStringAsFixed(0)} mph',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildStatCol(
|
||||
'MAX LEAN',
|
||||
'${math.max(ride.maxLeanLeft, ride.maxLeanRight).toStringAsFixed(0)}°',
|
||||
),
|
||||
_buildStatCol(
|
||||
'HARD BRK',
|
||||
'${ride.maxBrakingG.toStringAsFixed(2)}G',
|
||||
),
|
||||
_buildStatCol(
|
||||
'ELEV GAIN',
|
||||
'${ride.elevationGain.toStringAsFixed(0)}m',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isMultiColumn) {
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(12),
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 600,
|
||||
mainAxisExtent: 175,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
),
|
||||
itemCount: rides.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildRideCard(context, rides[index], colors);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
itemCount: rides.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildRideCard(context, rides[index], colors);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -210,45 +124,186 @@ class _HistoryScreenState extends ConsumerState<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(
|
||||
BuildContext context,
|
||||
List<GarageMotorcycle> bikes,
|
||||
GarageMotorcycle? defaultBike,
|
||||
) {
|
||||
if (bikes.isEmpty) return const SizedBox();
|
||||
if (bikes.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
final colors = context.motorColors;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.filter_alt, color: Colors.cyanAccent, size: 20),
|
||||
Icon(Icons.filter_alt, color: colors.cyberCyan, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white10,
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: colors.outlineVariant),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<int?>(
|
||||
value: _selectedMotorcycleId,
|
||||
isExpanded: true,
|
||||
dropdownColor: const Color(0xFF1E1E1E),
|
||||
dropdownColor: colors.surfaceContainerHigh,
|
||||
hint: const Text('Filter by Motorcycle'),
|
||||
items: [
|
||||
const DropdownMenuItem<int?>(
|
||||
value: null,
|
||||
child: Text('All Motorcycles'),
|
||||
child: Text('All Motorcycles', overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
if (defaultBike != null)
|
||||
DropdownMenuItem<int?>(
|
||||
value: -1,
|
||||
child: Text('Current (${defaultBike.modelName})'),
|
||||
child: Text('Current (${defaultBike.modelName})', overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
...bikes.map(
|
||||
(b) => DropdownMenuItem<int?>(
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)),
|
||||
Text(
|
||||
val,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 9.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
val,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
color: valueColor ?? theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/theme/evolution_theme_config.dart';
|
||||
import 'evolution_records_screen.dart';
|
||||
import '../../core/responsive/responsive_layout.dart';
|
||||
import '../../core/theme/theme_extensions.dart';
|
||||
import 'records_service.dart';
|
||||
|
||||
class RecordsScreen extends ConsumerWidget {
|
||||
|
|
@ -9,113 +9,511 @@ class RecordsScreen extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final themeMode = ref.watch(dashboardThemeModeProvider);
|
||||
if (themeMode == DashboardThemeMode.evolution) {
|
||||
return const EvolutionRecordsScreen();
|
||||
}
|
||||
|
||||
final records = ref.watch(recordsServiceProvider);
|
||||
final colors = context.motorColors;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Personal Records'),
|
||||
title: const Text(
|
||||
'Personal Records',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.emoji_events, color: Colors.amberAccent),
|
||||
icon: Icon(Icons.emoji_events, color: colors.warningAmber),
|
||||
tooltip: 'Trophies',
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
body: ResponsiveLayout(
|
||||
builder: (context, responsive) {
|
||||
final isMultiColumn = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
|
||||
|
||||
if (isMultiColumn) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionHeader('SPEED', colors),
|
||||
const SizedBox(height: 8),
|
||||
_buildHeroTopSpeedCard(context, records.topSpeedMph, colors),
|
||||
const SizedBox(height: 16),
|
||||
_buildSectionHeader('PERFORMANCE TIMES', colors),
|
||||
const SizedBox(height: 8),
|
||||
_buildPerformanceTimesCard(context, records, colors),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionHeader('HANDLING & DYNAMICS', colors),
|
||||
const SizedBox(height: 8),
|
||||
_buildHandlingCard(context, records, colors),
|
||||
const SizedBox(height: 16),
|
||||
_buildSectionHeader('ENDURANCE & ROUTE', colors),
|
||||
const SizedBox(height: 8),
|
||||
_buildEnduranceCard(context, records, colors),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionHeader('SPEED', colors),
|
||||
const SizedBox(height: 8),
|
||||
_buildHeroTopSpeedCard(context, records.topSpeedMph, colors),
|
||||
const SizedBox(height: 18),
|
||||
_buildSectionHeader('PERFORMANCE TIMES', colors),
|
||||
const SizedBox(height: 8),
|
||||
_buildPerformanceTimesCard(context, records, colors),
|
||||
const SizedBox(height: 18),
|
||||
_buildSectionHeader('HANDLING & DYNAMICS', colors),
|
||||
const SizedBox(height: 8),
|
||||
_buildHandlingCard(context, records, colors),
|
||||
const SizedBox(height: 18),
|
||||
_buildSectionHeader('ENDURANCE & ROUTE', colors),
|
||||
const SizedBox(height: 8),
|
||||
_buildEnduranceCard(context, records, colors),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(String title, MotorSignalThemeColors colors) {
|
||||
return Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Sora',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: colors.cyberCyan,
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeroTopSpeedCard(BuildContext context, double topSpeedMph, MotorSignalThemeColors colors) {
|
||||
final hasRecord = topSpeedMph > 0;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: colors.outlineVariant, width: 1.2),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionHeader('SPEED'),
|
||||
_buildRecordItem('Top Speed', records.topSpeedMph, 'MPH', isTime: false),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildSectionHeader('PERFORMANCE TIMES'),
|
||||
_buildRecordItem('Fastest 0-30', records.fastest0To30, 's', isTime: true),
|
||||
_buildRecordItem('Fastest 0-60', records.fastest0To60, 's', isTime: true),
|
||||
_buildRecordItem('Fastest 0-100', records.fastest0To100, 's', isTime: true),
|
||||
_buildRecordItem('Fastest 60-100', records.fastest60To100, 's', isTime: true),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildSectionHeader('HANDLING'),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(child: _buildRecordItem('Max Lean (Left)', records.maxLeanLeft, '°', isTime: false)),
|
||||
Expanded(child: _buildRecordItem('Max Lean (Right)', records.maxLeanRight, '°', isTime: false)),
|
||||
Text(
|
||||
'TOP SPEED',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
letterSpacing: 0.08,
|
||||
),
|
||||
),
|
||||
if (hasRecord)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.cyberCyan.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: colors.cyberCyan, width: 0.8),
|
||||
),
|
||||
child: Text(
|
||||
'RECORD',
|
||||
style: TextStyle(
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: colors.cyberCyan,
|
||||
letterSpacing: 0.05,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildRecordItem('Max Accel', records.maxAccelG, 'G', isTime: false)),
|
||||
Expanded(child: _buildRecordItem('Max Braking', records.maxBrakingG, 'G', isTime: false)),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
Center(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
hasRecord ? topSpeedMph.toStringAsFixed(0) : '--',
|
||||
style: TextStyle(
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fontSize: 54,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: theme.colorScheme.onSurface,
|
||||
letterSpacing: -0.02,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'MPH',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Sora',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: colors.cyberCyan,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildSectionHeader('ENDURANCE & ROUTE'),
|
||||
_buildRecordItem('Longest Ride', records.longestRideMiles, 'Miles', isTime: false),
|
||||
_buildRecordItem('Greatest Elevation Gain', records.greatestElevationGain, 'm', isTime: false),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Colors.cyanAccent,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 2,
|
||||
),
|
||||
Widget _buildPerformanceTimesCard(
|
||||
BuildContext context,
|
||||
PersonalRecords records,
|
||||
MotorSignalThemeColors colors,
|
||||
) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: colors.outlineVariant, width: 1.2),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildBenchmarkCell('0-30', records.fastest0To30, 3.0, colors, context),
|
||||
),
|
||||
Container(width: 1, height: 56, color: colors.outlineVariant),
|
||||
Expanded(
|
||||
child: _buildBenchmarkCell('0-60', records.fastest0To60, 6.0, colors, context),
|
||||
),
|
||||
],
|
||||
),
|
||||
Divider(color: colors.outlineVariant, height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildBenchmarkCell('0-100', records.fastest0To100, 12.0, colors, context),
|
||||
),
|
||||
Container(width: 1, height: 56, color: colors.outlineVariant),
|
||||
Expanded(
|
||||
child: _buildBenchmarkCell('60-100', records.fastest60To100, 8.0, colors, context),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecordItem(String label, double value, String unit, {required bool isTime}) {
|
||||
// If it's a time record and it's infinity, it means no record yet.
|
||||
// If it's a normal record and it's 0, it means no record yet.
|
||||
bool hasRecord = isTime ? value != double.infinity : value > 0;
|
||||
Widget _buildBenchmarkCell(
|
||||
String label,
|
||||
double value,
|
||||
double maxScale,
|
||||
MotorSignalThemeColors colors,
|
||||
BuildContext context,
|
||||
) {
|
||||
final hasRecord = value != double.infinity && value > 0;
|
||||
final frac = hasRecord ? (value / maxScale).clamp(0.1, 1.0) : 0.0;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
color: Colors.white10,
|
||||
margin: const EdgeInsets.only(bottom: 8, right: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(color: Colors.white70)),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
hasRecord ? (isTime ? value.toStringAsFixed(2) : value.toStringAsFixed(1)) : '--',
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
hasRecord ? '${value.toStringAsFixed(2)} s' : '--',
|
||||
style: TextStyle(
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasRecord) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
unit,
|
||||
style: const TextStyle(color: Colors.white54, fontSize: 14),
|
||||
Container(
|
||||
width: 6,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.cyberCyan,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
LinearProgressIndicator(
|
||||
value: frac,
|
||||
backgroundColor: colors.outlineVariant,
|
||||
valueColor: AlwaysStoppedAnimation<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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -5,7 +5,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||
import 'package:motorcycle_dash/core/theme/evolution_theme_config.dart';
|
||||
import 'package:motorcycle_dash/features/records/records_service.dart';
|
||||
import 'package:motorcycle_dash/features/dashboard/widgets/evolution_portrait_dashboard.dart';
|
||||
import 'package:motorcycle_dash/features/records/evolution_records_screen.dart';
|
||||
import 'package:motorcycle_dash/features/records/records_screen.dart';
|
||||
import 'package:motorcycle_dash/features/rides/ride_state_machine.dart';
|
||||
|
||||
void main() {
|
||||
|
|
@ -78,12 +78,17 @@ void main() {
|
|||
expect(find.text('START SESSION'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('EvolutionRecordsScreen renders speed records and performance times', (tester) async {
|
||||
testWidgets('RecordsScreen renders speed records and performance times', (tester) async {
|
||||
tester.view.physicalSize = const Size(1080, 2400);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(() => tester.view.resetPhysicalSize());
|
||||
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'topSpeedMph': 164.0,
|
||||
'fastest0To60': 2.9,
|
||||
'fastest0To100': 6.4,
|
||||
'maxLeanLeft': 48.0,
|
||||
});
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
await tester.pumpWidget(
|
||||
|
|
@ -92,15 +97,14 @@ void main() {
|
|||
sharedPreferencesProvider.overrideWithValue(prefs),
|
||||
],
|
||||
child: const MaterialApp(
|
||||
home: EvolutionRecordsScreen(),
|
||||
home: RecordsScreen(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('MOTOR SIGNAL'), findsOneWidget);
|
||||
expect(find.text('PERSONAL RECORDS'), findsOneWidget);
|
||||
expect(find.text('Personal Records'), findsOneWidget);
|
||||
expect(find.text('TOP SPEED'), findsOneWidget);
|
||||
expect(find.text('0-60'), findsOneWidget);
|
||||
expect(find.text('0-100'), findsOneWidget);
|
||||
|
|
@ -108,3 +112,4 @@ void main() {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:motorcycle_dash/database/database.dart';
|
||||
import 'package:motorcycle_dash/features/history/widgets/evolution_ride_details_view.dart';
|
||||
import 'package:motorcycle_dash/features/history/ride_detail_screen.dart';
|
||||
import 'package:motorcycle_dash/services/github/github_sync_service.dart';
|
||||
|
||||
class _FakeGitHubSyncService extends ChangeNotifier implements GitHubSyncService {
|
||||
|
|
@ -23,6 +24,16 @@ class _FakeGitHubSyncService extends ChangeNotifier implements GitHubSyncService
|
|||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
late AppDatabase db;
|
||||
|
||||
setUp(() {
|
||||
db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
Ride makeTestRide() {
|
||||
return Ride(
|
||||
id: 1,
|
||||
|
|
@ -54,12 +65,12 @@ void main() {
|
|||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
databaseProvider.overrideWithValue(db),
|
||||
githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()),
|
||||
],
|
||||
child: MaterialApp(
|
||||
home: EvolutionRideDetailsView(
|
||||
home: RideDetailScreen(
|
||||
ride: ride,
|
||||
events: const [],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -82,12 +93,41 @@ void main() {
|
|||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
databaseProvider.overrideWithValue(db),
|
||||
githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()),
|
||||
],
|
||||
child: MaterialApp(
|
||||
home: EvolutionRideDetailsView(
|
||||
home: RideDetailScreen(
|
||||
ride: ride,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.text('REPLAY RIDE'), findsOneWidget);
|
||||
expect(find.text('EXPORT GPX'), findsOneWidget);
|
||||
expect(find.text('EXPORT CSV'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders on landscape 800x360 screen without overflow', (tester) async {
|
||||
tester.view.physicalSize = const Size(800, 360);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(() => tester.view.resetPhysicalSize());
|
||||
|
||||
final ride = makeTestRide();
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
databaseProvider.overrideWithValue(db),
|
||||
githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()),
|
||||
],
|
||||
child: MaterialApp(
|
||||
home: RideDetailScreen(
|
||||
ride: ride,
|
||||
events: const [],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -101,3 +141,4 @@ void main() {
|
|||
expect(find.text('EXPORT CSV'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue