MotorSignal-App/lib/features/history/history_screen.dart

282 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:math' as math;
import '../../database/database.dart';
import '../garage/garage_providers.dart';
import 'ride_detail_screen.dart';
class HistoryScreen extends ConsumerStatefulWidget {
const HistoryScreen({super.key});
@override
ConsumerState<HistoryScreen> createState() => _HistoryScreenState();
}
class _HistoryScreenState extends ConsumerState<HistoryScreen> {
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);
return Scaffold(
appBar: AppBar(title: const Text('Ride History')),
body: Column(
children: [
_buildFilterBar(garageBikesAsync.value ?? [], defaultBike),
Expanded(
child: StreamBuilder<List<Ride>>(
stream: database.select(database.rides).watch(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
var rides = snapshot.data ?? [];
// Filter logic
if (_selectedMotorcycleId == -1 && defaultBike != null) {
rides = rides
.where((r) => r.motorcycleId == defaultBike.id)
.toList();
} else if (_selectedMotorcycleId != null &&
_selectedMotorcycleId! > 0) {
rides = rides
.where((r) => r.motorcycleId == _selectedMotorcycleId)
.toList();
}
// Sort by start time descending
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 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 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',
),
],
),
],
),
),
),
);
},
);
},
),
),
],
),
);
}
Widget _buildFilterBar(
List<GarageMotorcycle> bikes,
GarageMotorcycle? defaultBike,
) {
if (bikes.isEmpty) return const SizedBox();
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),
const SizedBox(width: 8),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.white10,
borderRadius: BorderRadius.circular(8),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<int?>(
value: _selectedMotorcycleId,
isExpanded: true,
dropdownColor: const Color(0xFF1E1E1E),
hint: const Text('Filter by Motorcycle'),
items: [
const DropdownMenuItem<int?>(
value: null,
child: Text('All Motorcycles'),
),
if (defaultBike != null)
DropdownMenuItem<int?>(
value: -1,
child: Text('Current (${defaultBike.modelName})'),
),
...bikes.map(
(b) => DropdownMenuItem<int?>(
value: b.id,
child: Text('${b.year} ${b.makeName} ${b.modelName}'),
),
),
],
onChanged: (val) {
setState(() {
_selectedMotorcycleId = val;
});
},
),
),
),
),
],
),
);
}
Widget _buildStatCol(String label, String val) {
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),
),
],
);
}
}