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

435 lines
18 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 '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);
}