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,197 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../api/connect/rmv/rmv_models.dart';
|
||||
import '../../../../api/errors/error_mapper.dart';
|
||||
import '../../../../routing/app_routes.dart';
|
||||
import '../../../../state/app/modules/rmv/repository/rmv_repository.dart';
|
||||
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../../widget/app_progress_indicator.dart';
|
||||
import '../favorites_controller.dart';
|
||||
import '../widgets/departure_arrival_tile.dart';
|
||||
|
||||
enum _Direction { departures, arrivals }
|
||||
|
||||
class StationDetailView extends StatefulWidget {
|
||||
final StopLocation station;
|
||||
const StationDetailView({super.key, required this.station});
|
||||
|
||||
@override
|
||||
State<StationDetailView> createState() => _StationDetailViewState();
|
||||
}
|
||||
|
||||
class _StationDetailViewState extends State<StationDetailView> {
|
||||
final RmvRepository _repo = RmvRepository();
|
||||
_Direction _direction = _Direction.departures;
|
||||
List<Departure>? _departures;
|
||||
List<Arrival>? _arrivals;
|
||||
bool _loading = false;
|
||||
Object? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
if (_direction == _Direction.departures) {
|
||||
final result = await _repo.departures(widget.station.id);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_departures = result;
|
||||
_loading = false;
|
||||
});
|
||||
} else {
|
||||
final result = await _repo.arrivals(widget.station.id);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_arrivals = result;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_error = e;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _switch(_Direction d) {
|
||||
if (d == _direction) return;
|
||||
setState(() {
|
||||
_direction = d;
|
||||
_departures = null;
|
||||
_arrivals = null;
|
||||
});
|
||||
_load();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settings = context.watch<SettingsCubit>();
|
||||
final favCtrl = RmvFavoritesController(settings);
|
||||
final isFav = favCtrl.isFavorite(widget.station);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.station.name),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: isFav ? 'Favorit entfernen' : 'Als Favorit speichern',
|
||||
icon: Icon(isFav ? Icons.star : Icons.star_border),
|
||||
onPressed: () => favCtrl.toggleFavorite(widget.station),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
||||
child: SegmentedButton<_Direction>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: _Direction.departures,
|
||||
icon: Icon(Icons.north_east),
|
||||
label: Text('Abfahrten'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: _Direction.arrivals,
|
||||
icon: Icon(Icons.south_west),
|
||||
label: Text('Ankünfte'),
|
||||
),
|
||||
],
|
||||
selected: {_direction},
|
||||
onSelectionChanged: (s) => _switch(s.first),
|
||||
),
|
||||
),
|
||||
Expanded(child: _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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_direction == _Direction.departures) {
|
||||
final list = _departures ?? const <Departure>[];
|
||||
if (list.isEmpty) return _emptyState('Keine Abfahrten gefunden.');
|
||||
return RefreshIndicator(
|
||||
onRefresh: _load,
|
||||
child: ListView.separated(
|
||||
itemCount: list.length,
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (_, i) => DepartureArrivalTile.fromDeparture(
|
||||
list[i],
|
||||
onTap: () => _openJourney(list[i].journeyRef),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final list = _arrivals ?? const <Arrival>[];
|
||||
if (list.isEmpty) return _emptyState('Keine Ankünfte gefunden.');
|
||||
return RefreshIndicator(
|
||||
onRefresh: _load,
|
||||
child: ListView.separated(
|
||||
itemCount: list.length,
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (_, i) => DepartureArrivalTile.fromArrival(
|
||||
list[i],
|
||||
onTap: () => _openJourney(list[i].journeyRef),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _emptyState(String text) => RefreshIndicator(
|
||||
onRefresh: _load,
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 80, horizontal: 24),
|
||||
child: Center(
|
||||
child: Text(text, style: Theme.of(context).textTheme.bodyLarge),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
void _openJourney(String? ref) {
|
||||
if (ref == null) return;
|
||||
AppRoutes.openRmvJourneyDetail(context, ref);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user