implemented RMV public transit module including trip search, station departures, and nearby stop lookup, added "Marianum Connect" API integration with bearer token authentication and auto-refresh logic, integrated geolocator for location-based station search, added persistent storage for favorite stations and recent trip queries, and implemented comprehensive UI for journey details, trip results, and disruption alerts
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user