implemented custom subject colors for the timetable and unified the color palette system

This commit is contained in:
2026-07-12 21:30:08 +02:00
parent a7111844b1
commit fe2b3c43b2
23 changed files with 916 additions and 89 deletions
@@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
/// Kuratierte Palette für Timetable-Farben (Fächer **und** eigene Termine). Die
/// Werte sind bewusst von den Status-Signalfarben in [lesson_color.dart]
/// abgesetzt (Marianum-Rot, Schwarz, Signallila, Signalblau, Event-Grün,
/// Duty-Petrol), damit gefärbte Regelstunden nicht mit Entfall/Vertretung
/// verwechselt werden. Der Server speichert nur den Enum-Namen; die konkrete
/// [Color] wird hier gemappt.
enum SubjectColor {
amber,
deepOrange,
pink,
purple,
indigo,
cyan,
teal,
lime,
brown,
blueGrey,
}
class SubjectColorPalette {
const SubjectColorPalette._();
/// Standardfarbe für neue eigene Termine (Fächer haben keinen Default —
/// dort bedeutet „keine Farbe" das Status-Rot).
static const SubjectColor defaultColor = SubjectColor.amber;
/// Altbestand: eigene Termine wurden früher mit der 4er-Palette
/// (orange/red/green/blue) gespeichert. Diese Namen bleiben auflösbar und
/// bilden auf die nächstliegende neue Farbe ab, damit bestehende Termine
/// nicht ihre Farbe verlieren.
static const Map<String, SubjectColor> _legacyAliases = {
'orange': SubjectColor.amber,
'red': SubjectColor.deepOrange,
'green': SubjectColor.teal,
'blue': SubjectColor.indigo,
};
static ColorModeDisplay displayOf(SubjectColor color) => switch (color) {
SubjectColor.amber => ColorModeDisplay(
color: Colors.amber.shade700,
displayName: 'Bernstein',
),
SubjectColor.deepOrange => ColorModeDisplay(
color: Colors.deepOrange.shade600,
displayName: 'Dunkelorange',
),
SubjectColor.pink => ColorModeDisplay(
color: Colors.pink.shade500,
displayName: 'Pink',
),
SubjectColor.purple => ColorModeDisplay(
color: Colors.deepPurple.shade400,
displayName: 'Violett',
),
SubjectColor.indigo => ColorModeDisplay(
color: Colors.indigo.shade400,
displayName: 'Indigo',
),
SubjectColor.cyan => ColorModeDisplay(
color: Colors.cyan.shade700,
displayName: 'Cyan',
),
SubjectColor.teal => ColorModeDisplay(
color: Colors.teal.shade400,
displayName: 'Türkis',
),
SubjectColor.lime => ColorModeDisplay(
color: Colors.lime.shade800,
displayName: 'Limette',
),
SubjectColor.brown => ColorModeDisplay(
color: Colors.brown.shade400,
displayName: 'Braun',
),
SubjectColor.blueGrey => ColorModeDisplay(
color: Colors.blueGrey.shade400,
displayName: 'Blaugrau',
),
};
/// Löst einen gespeicherten Namen (neue Palette oder Legacy-Alias) zum
/// [SubjectColor] auf. `null`, wenn der Name leer/unbekannt ist.
static SubjectColor? fromName(String? name) {
if (name == null || name.isEmpty) return null;
final match = SubjectColor.values.where((e) => e.name == name).firstOrNull;
return match ?? _legacyAliases[name];
}
/// Wie [fromName], liefert aber die konkrete [Color]. `null`, wenn der Name
/// leer/unbekannt ist — der Aufrufer fällt dann auf die Status-Farbe zurück.
static Color? colorFromName(String? name) {
final option = fromName(name);
return option == null ? null : displayOf(option).color;
}
}
class ColorModeDisplay {
final Color color;
final String displayName;
ColorModeDisplay({required this.color, required this.displayName});
}
@@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../../api/marianumconnect/models/mc_holiday.dart';
@@ -5,12 +6,12 @@ import '../../../../api/marianumconnect/queries/timetable_get_subjects/timetable
import '../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
import '../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
import '../../../../storage/timetable_settings.dart';
import '../custom_events/custom_event_colors.dart';
import 'arbitrary_appointment.dart';
import 'lesson_color.dart';
import 'lesson_status.dart';
import 'lesson_type_label.dart';
import 'rrule_with_exceptions.dart';
import 'subject_color_palette.dart';
import 'timetable_name_mode.dart';
class TimetableAppointmentFactory {
@@ -65,7 +66,7 @@ class TimetableAppointmentFactory {
endTime: endTime,
subject: _subjectName(subjectShortName, lesson),
location: _locationLabel(lesson),
color: LessonColor.forStatus(status),
color: _lessonColor(status, subjectShortName),
);
} catch (_) {
return Appointment(
@@ -131,9 +132,9 @@ class TimetableAppointmentFactory {
subject: _collapseWhitespace(event.title) ?? event.title,
recurrenceRule: parsed.rule,
recurrenceExceptionDates: exceptionDates.isEmpty ? null : exceptionDates,
color: TimetableColors.getColorFromString(
event.color ?? TimetableColors.defaultColor.name,
),
color:
SubjectColorPalette.colorFromName(event.color) ??
SubjectColorPalette.displayOf(SubjectColorPalette.defaultColor).color,
startTimeZone: '',
endTimeZone: '',
);
@@ -187,10 +188,32 @@ class TimetableAppointmentFactory {
e.second == 0;
}
/// Regelstunden (regulär/laufend/vergangen) übernehmen die persönliche
/// Fach-Farbe, sofern gesetzt. Sonderstatus (Entfall, Vertretung, Aufsicht,
/// Event) behalten immer ihre Signalfarbe, damit die Abgrenzung erhalten
/// bleibt.
Color _lessonColor(LessonStatus status, String? subjectShort) {
final isRegular =
status == LessonStatus.regular ||
status == LessonStatus.ongoing ||
status == LessonStatus.past;
if (isRegular) {
final custom = SubjectColorPalette.colorFromName(
_findSubject(subjectShort)?.color,
);
if (custom != null) return custom;
}
return LessonColor.forStatus(status);
}
McSubject? _findSubject(String? subjectShort) {
if (subjectShort == null) return null;
return subjects.where((s) => s.shortName == subjectShort).firstOrNull;
}
String _subjectName(String? subjectShort, McTimetableEntry lesson) {
if (subjectShort != null) {
final lookup =
subjects.where((s) => s.shortName == subjectShort).firstOrNull;
final lookup = _findSubject(subjectShort);
final name = switch (settings.timetableNameMode) {
// Backend liefert nur shortName + longName; alternateName fällt auf
// longName zurück.