QoL changes
All checks were successful
Release / meta (push) Successful in 22s
Release / linux-build (push) Successful in 4m32s
Release / android-build (push) Successful in 7m10s
Release / release-dev (push) Successful in 9s
Release / release-master (push) Successful in 9s

This commit is contained in:
2025-12-14 09:45:32 +00:00
parent 8116cfe7b1
commit f0dfbd185b
11 changed files with 887 additions and 321 deletions

View File

@@ -1,4 +1,7 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:mileograph_flutter/objects/objects.dart';
import 'package:mileograph_flutter/services/dataService.dart';
import 'package:provider/provider.dart';
@@ -155,25 +158,6 @@ class _LegsPageState extends State<LegsPage> {
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),
@@ -229,37 +213,7 @@ class _LegsPageState extends State<LegsPage> {
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,
),
)),
...legs.map((leg) => _buildLegCard(context, leg)),
const SizedBox(height: 8),
if (data.legsHasMore || data.isLegsLoading)
Align(
@@ -297,4 +251,148 @@ class _LegsPageState extends State<LegsPage> {
'${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
return '$dateStr · $timeStr';
}
Widget _buildLegCard(BuildContext context, Leg leg) {
final routeSegments = _parseRouteSegments(leg.route);
final textTheme = Theme.of(context).textTheme;
return Card(
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
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}',
style: textTheme.labelSmall,
),
if (leg.network.isNotEmpty)
Text(
leg.network,
style: textTheme.labelSmall,
),
],
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${leg.mileage.toStringAsFixed(1)} mi',
style:
textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700),
),
if (leg.tripId != 0)
Text(
'Trip #${leg.tripId}',
style: textTheme.labelSmall,
),
],
),
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (leg.notes.isNotEmpty) ...[
Text('Notes', style: textTheme.titleSmall),
const SizedBox(height: 4),
Text(leg.notes),
const SizedBox(height: 12),
],
if (leg.locos.isNotEmpty) ...[
Text('Locos', style: textTheme.titleSmall),
const SizedBox(height: 6),
Wrap(
spacing: 8,
runSpacing: 8,
children: _buildLocoChips(context, leg),
),
const SizedBox(height: 12),
],
if (routeSegments.isNotEmpty) ...[
Text('Route', style: textTheme.titleSmall),
const SizedBox(height: 6),
_buildRouteList(routeSegments),
],
],
),
),
],
),
);
}
List<Widget> _buildLocoChips(BuildContext context, Leg leg) {
final theme = Theme.of(context);
return leg.locos
.map(
(loco) => Chip(
label: Text('${loco.locoClass} ${loco.number}'),
avatar: const Icon(Icons.directions_railway, size: 16),
backgroundColor: theme.colorScheme.surfaceContainerHighest,
),
)
.toList();
}
Widget _buildRouteList(List<String> segments) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: segments
.map(
(segment) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: [
const Icon(Icons.circle, size: 10),
const SizedBox(width: 8),
Expanded(child: Text(segment)),
],
),
),
)
.toList(),
);
}
List<String> _parseRouteSegments(String route) {
final trimmed = route.trim();
if (trimmed.isEmpty) return [];
try {
final decoded = jsonDecode(trimmed);
if (decoded is List) {
return decoded.map((e) => e.toString()).toList();
}
} catch (_) {
// ignore and try alternative parsing
}
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
try {
final replaced = trimmed.replaceAll("'", '"');
final decoded = jsonDecode(replaced);
if (decoded is List) {
return decoded.map((e) => e.toString()).toList();
}
} catch (_) {}
}
if (trimmed.contains('->')) {
return trimmed
.split('->')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
}
if (trimmed.contains(',')) {
return trimmed
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
}
return [trimmed];
}
}