435 lines
18 KiB
Dart
435 lines
18 KiB
Dart
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 '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));
|
||
|
||
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}'),
|
||
),
|
||
actions: [
|
||
IconButton(
|
||
icon: const Icon(Icons.edit),
|
||
tooltip: 'Edit Nickname',
|
||
onPressed: () => _showEditNicknameDialog(context, ref, bike),
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.delete_outline, color: Colors.redAccent),
|
||
tooltip: 'Delete Motorcycle',
|
||
onPressed: () => _confirmDelete(context, ref, bike),
|
||
),
|
||
],
|
||
),
|
||
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),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildHeaderCard(BuildContext context, WidgetRef ref, GarageMotorcycle bike, MotorcycleSpecification? spec) {
|
||
return Card(
|
||
color: const Color(0xFF161616),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(16),
|
||
side: BorderSide(color: bike.isDefault ? Colors.cyanAccent : Colors.white12, width: 2),
|
||
),
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(20.0),
|
||
child: Column(
|
||
children: [
|
||
Row(
|
||
children: [
|
||
const Icon(Icons.two_wheeler, size: 54, color: Colors.cyanAccent),
|
||
const SizedBox(width: 16),
|
||
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),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
const Divider(color: Colors.white10),
|
||
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' : '—'),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
SizedBox(
|
||
width: double.infinity,
|
||
child: bike.isDefault
|
||
? Container(
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
decoration: BoxDecoration(
|
||
color: Colors.cyanAccent.withValues(alpha: 0.15),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: Colors.cyanAccent),
|
||
),
|
||
child: const Center(
|
||
child: Text(
|
||
'CURRENT MOTORCYCLE',
|
||
style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.5, color: Colors.cyanAccent),
|
||
),
|
||
),
|
||
)
|
||
: OutlinedButton(
|
||
style: OutlinedButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
side: const BorderSide(color: Colors.cyanAccent),
|
||
foregroundColor: Colors.cyanAccent,
|
||
),
|
||
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(fontWeight: FontWeight.bold, letterSpacing: 1.2)),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
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 _buildMyPerformanceSection(BuildContext context, AsyncValue<MotorcycleStats> statsAsync, MotorcycleSpecification? spec) {
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const Text(
|
||
'MY PERFORMANCE',
|
||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.cyanAccent, letterSpacing: 1.2),
|
||
),
|
||
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}')),
|
||
const SizedBox(width: 12),
|
||
Expanded(child: _buildPerfTile('TOTAL DISTANCE', '${stats.totalDistanceMiles.toStringAsFixed(1)} MI')),
|
||
],
|
||
),
|
||
const SizedBox(height: 12),
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: _buildPerfTile(
|
||
'BEST TOP SPEED',
|
||
'${stats.topSpeedMph.toStringAsFixed(1)} MPH',
|
||
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' : '—',
|
||
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)}°')),
|
||
const SizedBox(width: 12),
|
||
Expanded(child: _buildPerfTile('MAX ACCEL G', '${stats.maxAccelG.toStringAsFixed(2)} G')),
|
||
],
|
||
),
|
||
],
|
||
);
|
||
},
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildPerfTile(String label, String value, {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),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(label, style: const TextStyle(fontSize: 10, color: Colors.white54, letterSpacing: 1.0)),
|
||
const SizedBox(height: 4),
|
||
Text(value, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white)),
|
||
if (published != null) ...[
|
||
const SizedBox(height: 4),
|
||
Text('PUBLISHED: $published', style: const TextStyle(fontSize: 10, color: Colors.cyanAccent)),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildSpecificationSheet(BuildContext context, MotorcycleSpecification? spec) {
|
||
if (spec == null) {
|
||
return const Card(
|
||
child: 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: [
|
||
const Text(
|
||
'SPECIFICATIONS',
|
||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.cyanAccent, letterSpacing: 1.2),
|
||
),
|
||
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),
|
||
]),
|
||
_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),
|
||
]),
|
||
_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),
|
||
]),
|
||
_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),
|
||
]),
|
||
_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),
|
||
]),
|
||
_buildSpecGroup('TRANSMISSION', [
|
||
_SpecRow('Transmission Type', spec.transmissionType),
|
||
_SpecRow('Gears', spec.gearCount?.toString()),
|
||
_SpecRow('Final Drive', spec.finalDrive),
|
||
]),
|
||
if (spec.source != null) ...[
|
||
const SizedBox(height: 12),
|
||
Text(
|
||
'Specifications Source: ${spec.source}',
|
||
style: const TextStyle(fontSize: 12, color: Colors.white38),
|
||
),
|
||
],
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildSpecGroup(String title, List<_SpecRow> rows) {
|
||
final validRows = rows.where((r) => r.value != null && r.value!.isNotEmpty).toList();
|
||
if (validRows.isEmpty) return const SizedBox();
|
||
|
||
return Card(
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16.0),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(title, style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.cyanAccent, fontSize: 14, letterSpacing: 1.0)),
|
||
const SizedBox(height: 8),
|
||
...validRows.map(
|
||
(r) => Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(r.label, style: const TextStyle(color: Colors.white70)),
|
||
Flexible(
|
||
child: Text(
|
||
r.value!,
|
||
textAlign: TextAlign.right,
|
||
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
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) {
|
||
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.redAccent),
|
||
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);
|
||
}
|