initial codex commit
This commit is contained in:
@@ -1,31 +1,300 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:mileograph_flutter/services/dataService.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LegsPage extends StatefulWidget {
|
||||
const LegsPage({super.key});
|
||||
|
||||
class LegsPage extends StatelessWidget {
|
||||
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),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${leg.start} → ${leg.end}', style: TextStyle(fontSize: 16)),
|
||||
Text('Mileage: ${leg.mileage.toStringAsFixed(2)} km'),
|
||||
Text('Headcode: ${leg.headcode}'),
|
||||
Text('Begin: ${leg.beginTime}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@override
|
||||
State<LegsPage> createState() => _LegsPageState();
|
||||
}
|
||||
|
||||
class _LegsPageState extends State<LegsPage> {
|
||||
int _sortDirection = 0;
|
||||
DateTime? _startDate;
|
||||
DateTime? _endDate;
|
||||
bool _initialised = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (!_initialised) {
|
||||
_initialised = true;
|
||||
_refreshLegs();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshLegs() async {
|
||||
final data = context.read<DataService>();
|
||||
await data.fetchLegs(
|
||||
sortDirection: _sortDirection,
|
||||
dateRangeStart: _formatDate(_startDate),
|
||||
dateRangeEnd: _formatDate(_endDate),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMore() async {
|
||||
final data = context.read<DataService>();
|
||||
await data.fetchLegs(
|
||||
sortDirection: _sortDirection,
|
||||
dateRangeStart: _formatDate(_startDate),
|
||||
dateRangeEnd: _formatDate(_endDate),
|
||||
offset: data.legs.length,
|
||||
append: true,
|
||||
);
|
||||
}
|
||||
|
||||
double _pageMileage(List legs) {
|
||||
return legs.fold<double>(
|
||||
0,
|
||||
(prev, leg) => prev + (leg.mileage as double? ?? 0),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pickDate({required bool start}) async {
|
||||
final initial = start
|
||||
? _startDate ?? DateTime.now()
|
||||
: _endDate ?? _startDate ?? DateTime.now();
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: initial,
|
||||
firstDate: DateTime(1970),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
if (start) {
|
||||
_startDate = picked;
|
||||
if (_endDate != null && _endDate!.isBefore(picked)) {
|
||||
_endDate = picked;
|
||||
}
|
||||
} else {
|
||||
_endDate = picked;
|
||||
}
|
||||
});
|
||||
await _refreshLegs();
|
||||
}
|
||||
}
|
||||
|
||||
void _clearFilters() {
|
||||
setState(() {
|
||||
_startDate = null;
|
||||
_endDate = null;
|
||||
_sortDirection = 0;
|
||||
});
|
||||
_refreshLegs();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final data = context.watch<DataService>();
|
||||
final legs = data.legs;
|
||||
final pageMileage = _pageMileage(legs);
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshLegs,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Logbook',
|
||||
style: Theme.of(context).textTheme.labelMedium),
|
||||
const SizedBox(height: 2),
|
||||
Text('Entries',
|
||||
style: Theme.of(context).textTheme.headlineSmall),
|
||||
],
|
||||
),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('Page mileage',
|
||||
style: Theme.of(context).textTheme.labelSmall),
|
||||
Text('${pageMileage.toStringAsFixed(1)} mi',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w700)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Filters',
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
TextButton.icon(
|
||||
onPressed: _clearFilters,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Clear'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
SegmentedButton<int>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: 0,
|
||||
icon: Icon(Icons.south),
|
||||
label: Text('Newest first'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: 1,
|
||||
icon: Icon(Icons.north),
|
||||
label: Text('Oldest first'),
|
||||
),
|
||||
],
|
||||
selected: {_sortDirection},
|
||||
onSelectionChanged: (selection) {
|
||||
setState(() => _sortDirection = selection.first);
|
||||
_refreshLegs();
|
||||
},
|
||||
),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () => _pickDate(start: true),
|
||||
icon: const Icon(Icons.calendar_month),
|
||||
label: Text(
|
||||
_startDate == null
|
||||
? 'Start date'
|
||||
: _formatDate(_startDate!)!,
|
||||
),
|
||||
),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () => _pickDate(start: false),
|
||||
icon: const Icon(Icons.event),
|
||||
label: Text(
|
||||
_endDate == null
|
||||
? 'End date'
|
||||
: _formatDate(_endDate!)!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (data.isLegsLoading && legs.isEmpty)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
else if (legs.isEmpty)
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'No entries found',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text('Adjust the filters or add a new leg.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children: [
|
||||
...legs.map((leg) => Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.train),
|
||||
title: Text('${leg.start} → ${leg.end}'),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(_formatDateTime(leg.beginTime)),
|
||||
if (leg.headcode.isNotEmpty)
|
||||
Text('Headcode: ${leg.headcode}'),
|
||||
if (leg.route.isNotEmpty)
|
||||
Text(
|
||||
leg.route,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('${leg.mileage.toStringAsFixed(1)} mi'),
|
||||
Text(
|
||||
leg.network,
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
isThreeLine: true,
|
||||
),
|
||||
)),
|
||||
const SizedBox(height: 8),
|
||||
if (data.legsHasMore || data.isLegsLoading)
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed:
|
||||
data.isLegsLoading ? null : () => _loadMore(),
|
||||
icon: data.isLegsLoading
|
||||
? const SizedBox(
|
||||
height: 14,
|
||||
width: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.expand_more),
|
||||
label: Text(
|
||||
data.isLegsLoading ? 'Loading...' : 'Load more',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String? _formatDate(DateTime? date) {
|
||||
if (date == null) return null;
|
||||
return '${date.year.toString().padLeft(4, '0')}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
String _formatDateTime(DateTime date) {
|
||||
final dateStr = _formatDate(date) ?? '';
|
||||
final timeStr =
|
||||
'${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
|
||||
return '$dateStr · $timeStr';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user