Files
Client/lib/view/pages/rmv/widgets/leg_tile.dart
T

176 lines
5.5 KiB
Dart

import 'package:flutter/material.dart';
import '../../../../api/connect/rmv/rmv_models.dart';
import '../../../../extensions/date_time.dart';
import 'product_chip.dart';
import 'realtime_time.dart';
/// Renders a single [Leg] of a trip with header (line/direction), origin and
/// destination times and (optionally) the list of intermediate stops.
class LegTile extends StatelessWidget {
final Leg leg;
final VoidCallback? onShowJourneyDetail;
const LegTile({super.key, required this.leg, this.onShowJourneyDetail});
@override
Widget build(BuildContext context) {
final isWalk =
leg.type == LegType.walk || leg.type == LegType.transfer;
final cancelled = leg.cancelled || leg.partCancelled;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_header(context, isWalk: isWalk, cancelled: cancelled),
_endpoint(
context,
label: leg.origin.name,
scheduled: leg.origin.scheduledTime,
realtime: leg.origin.realTime,
delayMinutes: leg.origin.delayMinutes?.toInt(),
track: (leg.origin.realTrack?.isNotEmpty ?? false)
? leg.origin.realTrack
: leg.origin.track,
icon: Icons.trip_origin,
cancelled: cancelled,
),
if (leg.stops.length > 2)
_stopsExpander(context, leg.stops),
_endpoint(
context,
label: leg.destination.name,
scheduled: leg.destination.scheduledTime,
realtime: leg.destination.realTime,
delayMinutes: leg.destination.delayMinutes?.toInt(),
track: (leg.destination.realTrack?.isNotEmpty ?? false)
? leg.destination.realTrack
: leg.destination.track,
icon: Icons.place,
cancelled: cancelled,
),
],
),
);
}
Widget _header(
BuildContext context, {
required bool isWalk,
required bool cancelled,
}) {
final headlineStyle = Theme.of(context).textTheme.titleSmall?.copyWith(
decoration: cancelled ? TextDecoration.lineThrough : null,
);
final title = isWalk
? 'Fußweg'
: (leg.direction != null
? '${leg.name ?? ''}${leg.direction}'
: (leg.name ?? ''));
final canOpenJourney = leg.journeyRef != null && !isWalk;
return Container(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
padding: const EdgeInsets.fromLTRB(12, 8, 8, 8),
child: Row(
children: [
if (!isWalk) ProductChip(product: leg.product, fallbackLabel: leg.name),
if (!isWalk) const SizedBox(width: 8),
if (isWalk)
const Padding(
padding: EdgeInsets.only(right: 8),
child: Icon(Icons.directions_walk),
),
Expanded(
child: Text(
title,
style: headlineStyle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
if (leg.duration != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
'${leg.duration!.inMinutes} min',
style: Theme.of(context).textTheme.bodySmall,
),
),
if (canOpenJourney)
IconButton(
icon: const Icon(Icons.list_alt),
tooltip: 'Alle Halte',
onPressed: onShowJourneyDetail,
),
],
),
);
}
Widget _endpoint(
BuildContext context, {
required String label,
required DateTime scheduled,
DateTime? realtime,
int? delayMinutes,
String? track,
required IconData icon,
required bool cancelled,
}) => ListTile(
leading: Icon(icon, size: 20),
title: Text(
label,
style: TextStyle(
decoration: cancelled ? TextDecoration.lineThrough : null,
),
),
subtitle: (track != null && track.isNotEmpty)
? Text('Gleis $track')
: null,
trailing: RealtimeTime(
scheduled: scheduled,
realtime: realtime,
delayMinutes: delayMinutes,
cancelled: cancelled,
),
dense: true,
);
Widget _stopsExpander(BuildContext context, List<JourneyStop> stops) {
final intermediate = stops.length > 2
? stops.sublist(1, stops.length - 1)
: const <JourneyStop>[];
if (intermediate.isEmpty) return const SizedBox.shrink();
return ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 16),
childrenPadding: const EdgeInsets.symmetric(horizontal: 16),
title: Text(
'${intermediate.length} Zwischenhalt${intermediate.length > 1 ? 'e' : ''}',
style: Theme.of(context).textTheme.bodySmall,
),
children: intermediate
.map(
(s) => ListTile(
dense: true,
visualDensity: VisualDensity.compact,
leading: const Icon(Icons.fiber_manual_record, size: 10),
title: Text(s.name),
trailing: Text(_stopTime(s)),
),
)
.toList(),
);
}
String _stopTime(JourneyStop s) {
final dep = s.realDeparture ?? s.scheduledDeparture;
final arr = s.realArrival ?? s.scheduledArrival;
if (arr != null && dep != null && arr != dep) {
return '${arr.formatHm()} / ${dep.formatHm()}';
}
final t = dep ?? arr;
return t?.formatHm() ?? '';
}
}