initial codex commit
This commit is contained in:
@@ -1,28 +1,251 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:mileograph_flutter/objects/objects.dart';
|
||||
import 'package:mileograph_flutter/services/dataService.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class TripsPage extends StatelessWidget {
|
||||
class TripsPage extends StatefulWidget {
|
||||
const TripsPage({super.key});
|
||||
|
||||
@override
|
||||
State<TripsPage> createState() => _TripsPageState();
|
||||
}
|
||||
|
||||
class _TripsPageState extends State<TripsPage> {
|
||||
bool _initialised = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (!_initialised) {
|
||||
_initialised = true;
|
||||
_refreshTrips();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshTrips() async {
|
||||
await context.read<DataService>().fetchTripDetails();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final data = context.watch<DataService>();
|
||||
return ListView.builder(
|
||||
itemCount: data.legs.length,
|
||||
itemBuilder: (context, index) {
|
||||
final leg = data.legs[index];
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
final tripDetails = data.tripDetails;
|
||||
final tripSummaries = data.trips;
|
||||
final isMobile = MediaQuery.of(context).size.width < 700;
|
||||
final showLoading = data.isTripDetailsLoading && tripDetails.isEmpty;
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshTrips,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Journeys',
|
||||
style: Theme.of(context).textTheme.labelMedium),
|
||||
const SizedBox(height: 2),
|
||||
Text('Trips',
|
||||
style: Theme.of(context).textTheme.headlineSmall),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: _refreshTrips,
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: 'Refresh trips',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (showLoading)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
else if (tripDetails.isEmpty && tripSummaries.isEmpty)
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'No trips yet',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Use the Add entry flow to start grouping legs into trips.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (tripDetails.isEmpty)
|
||||
Column(
|
||||
children: tripSummaries
|
||||
.map(
|
||||
(trip) => Card(
|
||||
child: ListTile(
|
||||
title: Text(trip.tripName),
|
||||
subtitle:
|
||||
Text('${trip.tripMileage.toStringAsFixed(1)} mi'),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children:
|
||||
tripDetails.map((trip) => _buildTripCard(context, trip, isMobile)).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTripCard(BuildContext context, TripDetail trip, bool isMobile) {
|
||||
final legs = trip.legs;
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
trip.name,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
Text(
|
||||
'${trip.mileage.toStringAsFixed(1)} mi · ${trip.legCount} legs',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
tooltip: 'Details',
|
||||
onPressed: () => _showTripDetail(context, trip),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (legs.isNotEmpty)
|
||||
Column(
|
||||
children: legs.take(isMobile ? 2 : 3).map((leg) {
|
||||
return ListTile(
|
||||
dense: isMobile,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.train),
|
||||
title: Text('${leg.start} → ${leg.end}'),
|
||||
subtitle: Text(
|
||||
_formatDate(leg.beginTime),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Text(
|
||||
leg.mileage?.toStringAsFixed(1) ?? '-',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
if (legs.length > 3)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6.0),
|
||||
child: Text(
|
||||
'+${legs.length - 3} more legs',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDate(DateTime? date) {
|
||||
if (date == null) return '';
|
||||
return '${date.year.toString().padLeft(4, '0')}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
void _showTripDetail(BuildContext context, TripDetail trip) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (_) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${leg.start} → ${leg.end}',
|
||||
style: TextStyle(fontSize: 16),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
Text(
|
||||
trip.name,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Spacer(),
|
||||
Text('${trip.mileage.toStringAsFixed(1)} mi'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
child: ListView.builder(
|
||||
itemCount: trip.legs.length,
|
||||
itemBuilder: (context, index) {
|
||||
final leg = trip.legs[index];
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.train),
|
||||
title: Text('${leg.start} → ${leg.end}'),
|
||||
subtitle: Text(_formatDate(leg.beginTime)),
|
||||
trailing: Text(
|
||||
leg.mileage?.toStringAsFixed(1) ?? '-',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Text('Mileage: ${leg.mileage.toStringAsFixed(2)} km'),
|
||||
Text('Headcode: ${leg.headcode}'),
|
||||
Text('Begin: ${leg.beginTime}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user