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:
2026-05-20 19:08:05 +02:00
parent f185b3273a
commit 067012cc84
61 changed files with 7885 additions and 1 deletions
@@ -0,0 +1,183 @@
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 '../../../../widget/details_bottom_sheet.dart';
class DisruptionsView extends StatefulWidget {
const DisruptionsView({super.key});
@override
State<DisruptionsView> createState() => _DisruptionsViewState();
}
class _DisruptionsViewState extends State<DisruptionsView> {
final RmvRepository _repo = RmvRepository();
List<HimMessage>? _items;
bool _loading = true;
Object? _error;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
setState(() {
_loading = true;
_error = null;
});
try {
final r = await _repo.disruptions();
if (!mounted) return;
r.sort((a, b) => (b.priority ?? 0).compareTo(a.priority ?? 0));
setState(() {
_items = r;
_loading = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_error = e;
_loading = false;
});
}
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Störungsmeldungen')),
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: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
errorToUserMessage(err),
textAlign: TextAlign.center,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
const SizedBox(height: 12),
FilledButton.icon(
icon: const Icon(Icons.refresh),
onPressed: _load,
label: const Text('Erneut versuchen'),
),
],
),
),
);
}
final list = _items ?? const <HimMessage>[];
if (list.isEmpty) {
return RefreshIndicator(
onRefresh: _load,
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: const [
Padding(
padding: EdgeInsets.symmetric(vertical: 80, horizontal: 24),
child: Center(child: Text('Keine aktiven Meldungen.')),
),
],
),
);
}
return RefreshIndicator(
onRefresh: _load,
child: ListView.separated(
itemCount: list.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (_, i) => _tile(list[i]),
),
);
}
Widget _tile(HimMessage msg) => ListTile(
leading: Icon(
Icons.warning_amber_outlined,
color: _priorityColor(msg.priority),
),
title: Text(
msg.head ?? msg.lead ?? msg.text ?? 'Meldung',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
subtitle: msg.lead == null
? null
: Text(msg.lead!, maxLines: 2, overflow: TextOverflow.ellipsis),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showDetails(msg),
);
Color _priorityColor(int? priority) {
if (priority == null) return Colors.orange;
if (priority >= 100) return Colors.red;
if (priority >= 50) return Colors.orange;
return Colors.amber;
}
void _showDetails(HimMessage msg) {
showDetailsBottomSheet(
context,
header: Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 12),
child: Text(
msg.head ?? 'Störungsmeldung',
style: Theme.of(context).textTheme.titleLarge,
),
),
children: (ctx) => [
if (msg.lead != null)
ListTile(
leading: const Icon(Icons.short_text),
title: Text(msg.lead!),
),
if (msg.text != null)
ListTile(
leading: const Icon(Icons.notes),
title: Text(msg.text!),
),
if (msg.category != null)
ListTile(
leading: const Icon(Icons.category_outlined),
title: Text(msg.category!),
),
if (msg.company != null)
ListTile(
leading: const Icon(Icons.business_outlined),
title: Text(msg.company!),
),
if (msg.startValidity != null || msg.endValidity != null)
ListTile(
leading: const Icon(Icons.event_outlined),
title: Text(_validityRange(msg)),
),
if (msg.modified != null)
ListTile(
leading: const Icon(Icons.update_outlined),
title: Text('Aktualisiert: ${msg.modified!.formatDateTime()}'),
),
],
);
}
String _validityRange(HimMessage msg) {
final start = msg.startValidity?.formatDateTime();
final end = msg.endValidity?.formatDateTime();
if (start != null && end != null) return '$start $end';
return start ?? end ?? '';
}
}