MotorSignal-App/lib/features/garage/motorcycle_detail_screen.dart

697 lines
24 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:convert';
import 'package:flutter/material.dart';
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/responsive/responsive_layout.dart';
import '../../core/theme/theme_extensions.dart';
import 'widgets/motorcycle_photo.dart';
import 'garage_providers.dart';
class MotorcycleDetailScreen extends ConsumerWidget {
final int motorcycleId;
const MotorcycleDetailScreen({super.key, required this.motorcycleId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bikesAsync = ref.watch(garageMotorcyclesStreamProvider);
final statsAsync = ref.watch(motorcyclePerformanceStatsProvider(motorcycleId));
final colors = context.motorColors;
final theme = Theme.of(context);
return bikesAsync.when(
loading: () => const Scaffold(body: Center(child: CircularProgressIndicator())),
error: (err, _) => Scaffold(body: Center(child: Text('Error: $err'))),
data: (bikes) {
final bike = bikes.firstWhere(
(b) => b.id == motorcycleId,
orElse: () => GarageMotorcycle(
id: 0,
year: 2024,
makeId: '',
makeName: 'Unknown',
modelId: '',
modelName: 'Unknown',
isDefault: false,
dateAdded: DateTime.now(),
lastUpdated: DateTime.now(),
),
);
if (bike.id == 0) {
return Scaffold(
appBar: AppBar(),
body: const Center(child: Text('Motorcycle not found')),
);
}
MotorcycleSpecification? spec;
if (bike.specificationJson != null) {
try {
spec = MotorcycleSpecification.fromJson(json.decode(bike.specificationJson!));
} catch (_) {}
}
return Scaffold(
appBar: AppBar(
title: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
'${bike.year} ${bike.makeName} ${bike.modelName}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
actions: [
IconButton(
icon: const Icon(Icons.edit),
tooltip: 'Edit Nickname',
onPressed: () => _showEditNicknameDialog(context, ref, bike),
),
IconButton(
icon: Icon(Icons.delete_outline, color: colors.raceRed),
tooltip: 'Delete Motorcycle',
onPressed: () => _confirmDelete(context, ref, bike, colors),
),
],
),
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,
MotorSignalThemeColors colors,
ThemeData theme,
) {
return Card(
elevation: 2,
color: colors.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: bike.isDefault ? colors.cyberCyan : colors.outlineVariant,
width: bike.isDefault ? 1.8 : 1.0,
),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MotorcyclePhoto(
year: bike.year,
make: bike.makeName,
model: bike.modelName,
height: 190,
borderRadius: BorderRadius.circular(12),
showAttribution: true,
fit: BoxFit.cover,
),
const SizedBox(height: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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),
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' : '—',
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),
SizedBox(
width: double.infinity,
child: bike.isDefault
? Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: colors.cyberCyan.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: colors.cyberCyan),
),
child: Center(
child: Text(
'CURRENT MOTORCYCLE',
style: TextStyle(
fontFamily: 'Sora',
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
color: colors.cyberCyan,
),
),
),
)
: OutlinedButton(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
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.',
),
),
);
},
child: const Text(
'SET AS CURRENT MOTORCYCLE',
style: TextStyle(
fontFamily: 'Sora',
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
),
),
],
),
),
);
}
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,
MotorSignalThemeColors colors,
ThemeData theme,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'MY PERFORMANCE',
style: TextStyle(
fontFamily: 'Sora',
fontSize: 16,
fontWeight: FontWeight.bold,
color: colors.cyberCyan,
letterSpacing: 0.8,
),
),
const SizedBox(height: 12),
statsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => Text('Error loading stats: $err'),
data: (stats) {
return Column(
children: [
Row(
children: [
Expanded(child: _buildPerfTile('TOTAL RIDES', '${stats.rideCount}', colors, theme)),
const SizedBox(width: 12),
Expanded(
child: _buildPerfTile(
'TOTAL DISTANCE',
'${stats.totalDistanceMiles.toStringAsFixed(1)} MI',
colors,
theme,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildPerfTile(
'BEST TOP SPEED',
'${stats.topSpeedMph.toStringAsFixed(1)} MPH',
colors,
theme,
published: spec?.claimedTopSpeedMph != null
? '~${spec!.claimedTopSpeedMph!.toStringAsFixed(0)} MPH'
: null,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildPerfTile(
'BEST 0–60',
stats.best0to60Ms != null ? '${(stats.best0to60Ms! / 1000.0).toStringAsFixed(2)} s' : '—',
colors,
theme,
published: spec?.claimedZeroToSixtySec != null
? '~${spec!.claimedZeroToSixtySec!.toStringAsFixed(1)} s'
: null,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
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',
colors,
theme,
),
),
],
),
],
);
},
),
],
);
}
Widget _buildPerfTile(
String label,
String value,
MotorSignalThemeColors colors,
ThemeData theme, {
String? published,
}) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
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: TextStyle(
fontSize: 10,
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
fontWeight: FontWeight.w700,
letterSpacing: 0.8,
),
),
const SizedBox(height: 4),
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: TextStyle(
fontSize: 9.5,
color: colors.cyberCyan,
fontFamily: 'JetBrains Mono',
),
),
],
],
),
);
}
Widget _buildSpecificationSheet(
BuildContext context,
MotorcycleSpecification? spec,
MotorSignalThemeColors colors,
ThemeData theme,
) {
if (spec == null) {
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),
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'SPECIFICATIONS',
style: TextStyle(
fontFamily: 'Sora',
fontSize: 16,
fontWeight: FontWeight.bold,
color: colors.cyberCyan,
letterSpacing: 0.8,
),
),
const SizedBox(height: 12),
_buildSpecGroup('ENGINE', [
_SpecRow('Displacement', spec.displacementCc != null ? '${spec.displacementCc!.toStringAsFixed(0)} cc' : null),
_SpecRow('Configuration', spec.engineConfiguration),
_SpecRow('Cylinders', spec.cylinderCount?.toString()),
_SpecRow('Horsepower', spec.horsepower != null ? '${spec.horsepower!.toStringAsFixed(0)} HP @ ${spec.horsepowerRpm ?? "—"} RPM' : null),
_SpecRow('Torque', spec.torqueLbFt != null ? '${spec.torqueLbFt!.toStringAsFixed(1)} lb-ft @ ${spec.torqueRpm ?? "—"} RPM' : null),
_SpecRow('Redline', spec.redlineRpm != null ? '${spec.redlineRpm} RPM' : null),
_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),
_SpecRow('Rear Suspension', spec.rearSuspension),
_SpecRow('Front Brake', spec.frontBrake),
_SpecRow('Rear Brake', spec.rearBrake),
_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: TextStyle(
fontSize: 11,
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
fontFamily: 'Inter',
),
),
],
],
);
}
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.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(14.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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: TextStyle(
fontWeight: FontWeight.bold,
fontFamily: 'JetBrains Mono',
fontSize: 12,
color: theme.colorScheme.onSurface,
),
),
),
],
),
),
),
],
),
),
);
}
void _showEditNicknameDialog(BuildContext context, WidgetRef ref, GarageMotorcycle bike) {
final controller = TextEditingController(text: bike.nickname ?? '');
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Edit Nickname'),
content: TextField(
controller: controller,
decoration: const InputDecoration(hintText: 'Enter nickname'),
),
actions: [
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,
);
Navigator.pop(context);
},
child: const Text('Save'),
),
],
);
},
);
}
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.',
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: colors.raceRed),
onPressed: () {
ref.read(garageControllerProvider).deleteMotorcycle(bike.id);
Navigator.pop(context);
context.pop();
},
child: const Text('Delete', style: TextStyle(color: Colors.white)),
),
],
);
},
);
}
}
class _SpecRow {
final String label;
final String? value;
_SpecRow(this.label, this.value);
}