220 lines
9.5 KiB
Dart
220 lines
9.5 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(
|
|
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)),
|
|
],
|
|
);
|
|
}
|
|
}
|