196 lines
6.0 KiB
Dart
196 lines
6.0 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:mileograph_flutter/objects/objects.dart';
|
|
|
|
class LegCard extends StatelessWidget {
|
|
const LegCard({
|
|
super.key,
|
|
required this.leg,
|
|
this.showEditButton = true,
|
|
});
|
|
|
|
final Leg leg;
|
|
final bool showEditButton;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
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: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
Text(
|
|
'${leg.mileage.toStringAsFixed(1)} mi',
|
|
style: textTheme.labelLarge?.copyWith(
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
if (leg.tripId != 0) ...[
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
'Trip #${leg.tripId}',
|
|
style: textTheme.labelSmall,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
if (showEditButton) ...[
|
|
const SizedBox(width: 8),
|
|
IconButton(
|
|
tooltip: 'Edit entry',
|
|
icon: const Icon(Icons.edit),
|
|
visualDensity: VisualDensity.compact,
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
|
onPressed: () => context.push('/legs/edit/${leg.id}'),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
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),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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')}';
|
|
}
|
|
|
|
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';
|
|
}
|
|
|
|
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 (_) {}
|
|
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];
|
|
}
|
|
}
|
|
|