Files
Client/lib/view/pages/rmv/journey/journey_detail_view.dart
T

169 lines
4.8 KiB
Dart

import 'package:flutter/material.dart';
import '../../../../api/connect/rmv/rmv_models.dart';
import '../../../../api/errors/error_mapper.dart';
import '../../../../extensions/date_time.dart';
import '../../../../state/app/modules/rmv/repository/rmv_repository.dart';
import '../../../../widget/app_progress_indicator.dart';
import '../widgets/product_chip.dart';
import '../widgets/realtime_time.dart';
class JourneyDetailView extends StatefulWidget {
final String journeyRef;
final DateTime? date;
const JourneyDetailView({super.key, required this.journeyRef, this.date});
@override
State<JourneyDetailView> createState() => _JourneyDetailViewState();
}
class _JourneyDetailViewState extends State<JourneyDetailView> {
final RmvRepository _repo = RmvRepository();
JourneyDetail? _detail;
bool _loading = true;
Object? _error;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
setState(() {
_loading = true;
_error = null;
});
try {
final detail = await _repo.journeyDetail(
widget.journeyRef,
date: widget.date,
);
if (!mounted) return;
setState(() {
_detail = detail;
_loading = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_error = e;
_loading = false;
});
}
}
@override
Widget build(BuildContext context) {
final detail = _detail;
return Scaffold(
appBar: AppBar(
title: Row(
children: [
ProductChip(product: detail?.product),
const SizedBox(width: 8),
Expanded(
child: Text(
detail?.direction ?? 'Fahrt',
overflow: TextOverflow.ellipsis,
),
),
],
),
),
body: _body(),
);
}
Widget _body() {
if (_loading) {
return const Center(child: AppProgressIndicator.large());
}
final err = _error;
if (err != null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
errorToUserMessage(err),
textAlign: TextAlign.center,
),
),
);
}
final stops = _detail?.stops ?? const <JourneyStop>[];
if (stops.isEmpty) {
return const Center(child: Text('Keine Halte verfügbar.'));
}
return RefreshIndicator(
onRefresh: _load,
child: ListView.separated(
itemCount: stops.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (_, i) => _stopTile(stops[i], i, stops.length),
),
);
}
Widget _stopTile(JourneyStop stop, int idx, int total) {
final isFirst = idx == 0;
final isLast = idx == total - 1;
final arrival = stop.realArrival ?? stop.scheduledArrival;
final departure = stop.realDeparture ?? stop.scheduledDeparture;
final track = (stop.realDepTrack?.isNotEmpty ?? false)
? stop.realDepTrack
: stop.depTrack;
return ListTile(
leading: SizedBox(
width: 24,
child: Icon(
isFirst
? Icons.trip_origin
: (isLast ? Icons.place : Icons.fiber_manual_record),
size: isFirst || isLast ? 20 : 10,
color: stop.cancelled
? Colors.red
: Theme.of(context).colorScheme.primary,
),
),
title: Text(
stop.name,
style: TextStyle(
decoration: stop.cancelled ? TextDecoration.lineThrough : null,
fontWeight: (isFirst || isLast) ? FontWeight.bold : null,
),
),
subtitle: (track == null || track.isEmpty)
? null
: Text('Gleis $track'),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (stop.scheduledArrival != null)
RealtimeTime(
scheduled: stop.scheduledArrival!,
realtime: stop.realArrival,
cancelled: stop.cancelledArrival,
style: Theme.of(context).textTheme.bodyMedium,
),
if (stop.scheduledDeparture != null &&
stop.scheduledDeparture != stop.scheduledArrival)
RealtimeTime(
scheduled: stop.scheduledDeparture!,
realtime: stop.realDeparture,
cancelled: stop.cancelledDeparture,
style: Theme.of(context).textTheme.bodyMedium,
),
if (arrival == null && departure == null) const Text('-'),
],
),
isThreeLine: arrival != null && departure != null && arrival != departure,
);
}
}
/// Allows showing "departure 14:35" as a tooltip in the journey timeline.
String formatStopMoment(DateTime t) => t.formatHm();