109 lines
3.6 KiB
Dart
109 lines
3.6 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';
|
||
|
||
/// Compact summary of a [Trip] used in the trip results list.
|
||
class TripTile extends StatelessWidget {
|
||
final Trip trip;
|
||
final VoidCallback? onTap;
|
||
|
||
const TripTile({super.key, required this.trip, this.onTap});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final firstLeg = trip.legs.isEmpty ? null : trip.legs.first;
|
||
final lastLeg = trip.legs.isEmpty ? null : trip.legs.last;
|
||
if (firstLeg == null || lastLeg == null) {
|
||
return const ListTile(title: Text('Verbindung ohne Halt'));
|
||
}
|
||
final scheduledStart = firstLeg.origin.scheduledTime;
|
||
final scheduledEnd = lastLeg.destination.scheduledTime;
|
||
final cancelled =
|
||
trip.legs.any((l) => l.cancelled || l.partCancelled);
|
||
final transfers = trip.transferCount ?? _countTransfers(trip);
|
||
final duration = trip.realDuration ?? trip.duration;
|
||
final productChips = trip.legs
|
||
.where((l) => l.type == LegType.journey && l.product != null)
|
||
.map((l) => Padding(
|
||
padding: const EdgeInsets.only(right: 4),
|
||
child: ProductChip(product: l.product, fallbackLabel: l.name),
|
||
))
|
||
.toList();
|
||
|
||
return ListTile(
|
||
onTap: onTap,
|
||
isThreeLine: true,
|
||
title: Row(
|
||
children: [
|
||
RealtimeTime(
|
||
scheduled: scheduledStart,
|
||
realtime: firstLeg.origin.realTime,
|
||
delayMinutes: firstLeg.origin.delayMinutes?.toInt(),
|
||
cancelled: cancelled,
|
||
style: Theme.of(context).textTheme.titleMedium,
|
||
),
|
||
const Padding(
|
||
padding: EdgeInsets.symmetric(horizontal: 6),
|
||
child: Text('–'),
|
||
),
|
||
RealtimeTime(
|
||
scheduled: scheduledEnd,
|
||
realtime: lastLeg.destination.realTime,
|
||
delayMinutes: lastLeg.destination.delayMinutes?.toInt(),
|
||
cancelled: cancelled,
|
||
style: Theme.of(context).textTheme.titleMedium,
|
||
),
|
||
],
|
||
),
|
||
subtitle: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const SizedBox(height: 4),
|
||
Wrap(runSpacing: 4, children: productChips),
|
||
const SizedBox(height: 4),
|
||
Row(
|
||
children: [
|
||
if (duration != null) ...[
|
||
const Icon(Icons.schedule, size: 14),
|
||
const SizedBox(width: 2),
|
||
Text(_formatDuration(duration)),
|
||
const SizedBox(width: 12),
|
||
],
|
||
const Icon(Icons.swap_horiz, size: 14),
|
||
const SizedBox(width: 2),
|
||
Text(
|
||
transfers == 0
|
||
? 'Direkt'
|
||
: '$transfers Umstieg${transfers > 1 ? 'e' : ''}',
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
trailing: const Icon(Icons.chevron_right),
|
||
);
|
||
}
|
||
|
||
int _countTransfers(Trip trip) {
|
||
final journeyLegs =
|
||
trip.legs.where((l) => l.type == LegType.journey).length;
|
||
return journeyLegs <= 1 ? 0 : journeyLegs - 1;
|
||
}
|
||
}
|
||
|
||
String _formatDuration(Duration d) {
|
||
final hours = d.inHours;
|
||
final minutes = d.inMinutes.remainder(60);
|
||
if (hours == 0) return '$minutes min';
|
||
return '$hours h ${minutes.toString().padLeft(2, '0')} min';
|
||
}
|
||
|
||
/// Re-export for trip detail screen.
|
||
String formatTripDuration(Duration d) => _formatDuration(d);
|
||
|
||
/// Helper used in date headers on the trip results list.
|
||
String formatTripDateHeader(DateTime when) => when.formatDateRelativeShort();
|