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

359 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:math' as math;
import 'package:intl/intl.dart';
import '../../database/database.dart';
import '../../core/responsive/responsive_layout.dart';
import '../../core/theme/theme_extensions.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);
final colors = context.motorColors;
return Scaffold(
appBar: AppBar(
title: const Text(
'Ride History',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
body: Column(
children: [
_buildFilterBar(context, 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 Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.history,
size: 64,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3),
),
const SizedBox(height: 16),
Text(
'No recorded rides matching filter.',
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
),
],
),
);
}
return ResponsiveLayout(
builder: (context, responsive) {
final isMultiColumn = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
if (isMultiColumn) {
return GridView.builder(
padding: const EdgeInsets.all(12),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 600,
mainAxisExtent: 175,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: rides.length,
itemBuilder: (context, index) {
return _buildRideCard(context, rides[index], colors);
},
);
}
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
itemCount: rides.length,
itemBuilder: (context, index) {
return _buildRideCard(context, rides[index], colors);
},
);
},
);
},
),
),
],
),
);
}
Widget _buildRideCard(BuildContext context, Ride ride, MotorSignalThemeColors colors) {
final bikeName = (ride.motorcycleYear != null &&
ride.motorcycleMake != null &&
ride.motorcycleModel != null)
? '${ride.motorcycleYear} ${ride.motorcycleMake} ${ride.motorcycleModel}'
: null;
final theme = Theme.of(context);
final dateFormatted = DateFormat('yyyy-MM-dd HH:mm').format(ride.startTime.toLocal());
return Card(
elevation: 2,
margin: const EdgeInsets.symmetric(vertical: 6),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: colors.outlineVariant),
),
color: colors.surfaceContainer,
child: InkWell(
key: ValueKey('history_ride_card_${ride.id}'),
borderRadius: BorderRadius.circular(12),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => RideDetailScreen(ride: ride),
),
);
},
child: Padding(
padding: const EdgeInsets.all(14.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Ride ${ride.id}',
style: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 16,
fontFamily: 'Sora',
color: theme.colorScheme.onSurface,
),
),
if (bikeName != null) ...[
const SizedBox(width: 6),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 5,
vertical: 2,
),
decoration: BoxDecoration(
color: colors.cyberCyan.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: colors.cyberCyan.withValues(alpha: 0.4),
),
),
child: Text(
bikeName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 10.5,
fontWeight: FontWeight.bold,
color: colors.cyberCyan,
),
),
),
),
] else ...[
const Spacer(),
],
const SizedBox(width: 6),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerRight,
child: Text(
dateFormatted,
style: TextStyle(
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
fontSize: 10.5,
fontFamily: 'JetBrains Mono',
),
),
),
),
],
),
const SizedBox(height: 8),
Divider(height: 1, color: colors.outlineVariant),
const SizedBox(height: 10),
Row(
children: [
Expanded(child: _buildStatCol(context, 'TRIP', '${ride.distanceMiles.toStringAsFixed(1)} mi')),
Expanded(child: _buildStatCol(context, 'TIME', '${(ride.movingTime / 60).toStringAsFixed(0)} min')),
Expanded(child: _buildStatCol(context, 'TOP SPD', '${ride.topSpeedMph.toStringAsFixed(0)} mph')),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildStatCol(
context,
'MAX LEAN',
'${math.max(ride.maxLeanLeft, ride.maxLeanRight).toStringAsFixed(0)}°',
valueColor: colors.cyberCyan,
),
),
Expanded(
child: _buildStatCol(
context,
'HARD BRK',
'${ride.maxBrakingG.toStringAsFixed(2)}G',
valueColor: colors.gForceNegative,
),
),
Expanded(
child: _buildStatCol(
context,
'ELEV GAIN',
'${ride.elevationGain.toStringAsFixed(0)}m',
),
),
],
),
],
),
),
),
);
}
Widget _buildFilterBar(
BuildContext context,
List<GarageMotorcycle> bikes,
GarageMotorcycle? defaultBike,
) {
if (bikes.isEmpty) return const SizedBox.shrink();
final colors = context.motorColors;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
children: [
Icon(Icons.filter_alt, color: colors.cyberCyan, size: 20),
const SizedBox(width: 8),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: colors.outlineVariant),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<int?>(
value: _selectedMotorcycleId,
isExpanded: true,
dropdownColor: colors.surfaceContainerHigh,
hint: const Text('Filter by Motorcycle'),
items: [
const DropdownMenuItem<int?>(
value: null,
child: Text('All Motorcycles', overflow: TextOverflow.ellipsis),
),
if (defaultBike != null)
DropdownMenuItem<int?>(
value: -1,
child: Text('Current (${defaultBike.modelName})', overflow: TextOverflow.ellipsis),
),
...bikes.map(
(b) => DropdownMenuItem<int?>(
value: b.id,
child: Text('${b.year} ${b.makeName} ${b.modelName}', overflow: TextOverflow.ellipsis),
),
),
],
onChanged: (val) {
setState(() {
_selectedMotorcycleId = val;
});
},
),
),
),
),
],
),
);
}
Widget _buildStatCol(BuildContext context, String label, String val, {Color? valueColor}) {
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 9.5,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
),
),
const SizedBox(height: 2),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
val,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
fontFamily: 'JetBrains Mono',
color: valueColor ?? theme.colorScheme.onSurface,
),
),
),
],
);
}
}