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 stops) { final intermediate = stops.length > 2 ? stops.sublist(1, stops.length - 1) : const []; 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() ?? ''; } }