implemented RMV commute integration in the timetable, added Nominatim geocoding for home station lookup, created CommuteCubit for daily trip management with TTL caching, and introduced specialized timetable tiles, detail sheets, and settings for transit connections and walking buffers

This commit is contained in:
2026-05-20 22:50:57 +02:00
parent 067012cc84
commit 46d6b3410e
20 changed files with 1513 additions and 20 deletions
@@ -1,5 +1,7 @@
import '../../../../api/connect/rmv/rmv_models.dart';
import '../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
import '../../../../api/webuntis/queries/get_timetable/get_timetable_response.dart';
import 'commute_direction.dart';
sealed class ArbitraryAppointment {
const ArbitraryAppointment();
@@ -7,9 +9,12 @@ sealed class ArbitraryAppointment {
T when<T>({
required T Function(GetTimetableResponseObject lesson) webuntis,
required T Function(CustomTimetableEvent event) custom,
required T Function(Trip trip, CommuteDirection direction) commute,
}) => switch (this) {
WebuntisAppointment(:final lesson) => webuntis(lesson),
CustomAppointment(:final event) => custom(event),
CommuteAppointment(:final trip, :final direction) =>
commute(trip, direction),
};
}
@@ -22,3 +27,9 @@ class CustomAppointment extends ArbitraryAppointment {
final CustomTimetableEvent event;
const CustomAppointment(this.event);
}
class CommuteAppointment extends ArbitraryAppointment {
final Trip trip;
final CommuteDirection direction;
const CommuteAppointment(this.trip, this.direction);
}
@@ -282,8 +282,9 @@ class LaidOutOverflow extends LaidOutCell {
int _appointmentPriority(Appointment a) {
final id = a.id;
if (id is CustomAppointment) return 0;
if (id is WebuntisAppointment && id.lesson.code == 'cancelled') return 1;
return 2;
if (id is CommuteAppointment) return 1;
if (id is WebuntisAppointment && id.lesson.code == 'cancelled') return 2;
return 3;
}
/// Assigns each appointment a lane index using a greedy sweep, then collapses
@@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../../api/connect/rmv/rmv_models.dart';
import '../../../../extensions/date_time.dart';
import 'arbitrary_appointment.dart';
import 'commute_direction.dart';
/// Builds [Appointment] objects from RMV [Trip]s so the existing timetable
/// lane layout can render them next to school lessons. Per-trip cancellation
/// flips the color to red; the [appointment.id] always wraps the original
/// [Trip] so the tap handler can surface its details.
class CommuteAppointmentFactory {
static const Color colorMorning = Color(0xFFFB8C00); // amber 600
static const Color colorEvening = Color(0xFF8E24AA); // purple 600
static const Color colorCancelled = Color(0xFFE53935); // red 600
/// Converts every entry in [morning]/[evening] into an Appointment.
static List<Appointment> build({
required List<Trip> morning,
required List<Trip> evening,
}) => [
for (final trip in morning) ?_tripToAppointment(trip, CommuteDirection.toSchool),
for (final trip in evening) ?_tripToAppointment(trip, CommuteDirection.fromSchool),
];
static Appointment? _tripToAppointment(Trip trip, CommuteDirection direction) {
final firstLeg = trip.legs.firstOrNull;
final lastLeg = trip.legs.lastOrNull;
if (firstLeg == null || lastLeg == null) return null;
final start = firstLeg.origin.scheduledTime;
final end = lastLeg.destination.scheduledTime;
if (!end.isAfter(start)) return null;
final cancelled =
trip.legs.every((l) => l.cancelled || l.partCancelled) &&
trip.legs.isNotEmpty;
final color = cancelled
? colorCancelled
: (direction == CommuteDirection.toSchool
? colorMorning
: colorEvening);
return Appointment(
id: CommuteAppointment(trip, direction),
startTime: start,
endTime: end,
subject: _subject(trip),
location: _location(direction, start, end),
color: color,
startTimeZone: '',
endTimeZone: '',
);
}
static String _subject(Trip trip) {
final lines = trip.legs
.where((l) => l.type == LegType.journey)
.map(_legLabel)
.where((s) => s.isNotEmpty)
.toList();
if (lines.isEmpty) return 'Fußweg';
return lines.join(' ');
}
static String _legLabel(Leg leg) {
final p = leg.product;
if (p == null) return leg.name ?? '?';
if (p.line != null && p.line!.isNotEmpty) return p.line!;
if (p.displayNumber != null && p.displayNumber!.isNotEmpty) {
return '${p.category ?? ''}${p.displayNumber}'.trim();
}
return p.name ?? leg.name ?? '?';
}
static String _location(
CommuteDirection direction,
DateTime start,
DateTime end,
) {
final label = direction == CommuteDirection.toSchool ? 'Hinfahrt' : 'Heimfahrt';
return '$label\n${start.formatHm()}${end.formatHm()}';
}
}
@@ -0,0 +1,2 @@
/// Direction of a commute trip relative to the school day.
enum CommuteDirection { toSchool, fromSchool }