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,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 ?? '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user