show classname instead of teacher name in teacher timetable view
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import '../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||
|
||||
final RegExp _whitespaceRun = RegExp(r'\s+');
|
||||
|
||||
/// Collapses any line-break or whitespace run to a single space and trims.
|
||||
/// Returns null when input is null or fully whitespace. Webuntis sometimes
|
||||
/// returns multi-line values like "A30\n4" — this normalizes those so labels
|
||||
/// render on a single line.
|
||||
String? collapseWhitespace(String? s) {
|
||||
if (s == null) return null;
|
||||
final cleaned = s.replaceAll(_whitespaceRun, ' ').trim();
|
||||
return cleaned.isEmpty ? null : cleaned;
|
||||
}
|
||||
|
||||
/// "7a, 7b" — shared by the calendar tile factory and the home-widget mapper
|
||||
/// so both surfaces render identical class labels on teacher plans.
|
||||
extension LessonClassLabel on McTimetableEntry {
|
||||
String? get classLabel => collapseWhitespace(classNames.join(', '));
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||
|
||||
/// Combines back-to-back lessons with identical subject/room/teacher/status
|
||||
@@ -44,6 +46,9 @@ class LessonMerger {
|
||||
b.teachers.firstOrNull?.shortName) {
|
||||
return false;
|
||||
}
|
||||
// Relevant für Lehrerpläne: gleicher Lehrer/Fach/Raum, aber verschiedene
|
||||
// Klassen dürfen nicht zu einem Block verschmelzen.
|
||||
if (!listEquals(a.classNames, b.classNames)) return false;
|
||||
if (a.status != b.status) return false;
|
||||
// Lower bound on the gap — without it, two identical-metadata lessons that
|
||||
// overlap in time would silently collapse into one.
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart'
|
||||
import '../../../../storage/timetable_settings.dart';
|
||||
import 'arbitrary_appointment.dart';
|
||||
import 'lesson_color.dart';
|
||||
import 'lesson_labels.dart';
|
||||
import 'lesson_merger.dart';
|
||||
import 'lesson_status.dart';
|
||||
import 'lesson_type_label.dart';
|
||||
@@ -23,6 +24,10 @@ class TimetableAppointmentFactory {
|
||||
final TimetableSettings settings;
|
||||
final DateTime now;
|
||||
|
||||
/// Teacher plans (a teacher's own plan or a foreign teacher view) show the
|
||||
/// class on the tile instead of the teacher's own name.
|
||||
final bool showClassInsteadOfTeacher;
|
||||
|
||||
TimetableAppointmentFactory({
|
||||
required this.lessons,
|
||||
required this.customEvents,
|
||||
@@ -30,6 +35,7 @@ class TimetableAppointmentFactory {
|
||||
required this.settings,
|
||||
required this.now,
|
||||
this.holidays = const [],
|
||||
this.showClassInsteadOfTeacher = false,
|
||||
});
|
||||
|
||||
List<Appointment> build() {
|
||||
@@ -130,7 +136,7 @@ class TimetableAppointmentFactory {
|
||||
location: event.description.trim().isEmpty
|
||||
? null
|
||||
: event.description.trim(),
|
||||
subject: _collapseWhitespace(event.title) ?? event.title,
|
||||
subject: collapseWhitespace(event.title) ?? event.title,
|
||||
recurrenceRule: parsed.rule,
|
||||
recurrenceExceptionDates: exceptionDates.isEmpty ? null : exceptionDates,
|
||||
color:
|
||||
@@ -222,7 +228,7 @@ class TimetableAppointmentFactory {
|
||||
TimetableNameMode.longName => lookup?.longName ?? subjectShort,
|
||||
TimetableNameMode.alternateName => lookup?.longName ?? subjectShort,
|
||||
};
|
||||
final collapsed = _collapseWhitespace(name);
|
||||
final collapsed = collapseWhitespace(name);
|
||||
if (collapsed != null) return collapsed;
|
||||
}
|
||||
// Subject leer → Titel aus dem Lesson-Type ableiten. Pausenaufsicht etc.
|
||||
@@ -233,10 +239,13 @@ class TimetableAppointmentFactory {
|
||||
|
||||
String _locationLabel(McTimetableEntry lesson) {
|
||||
final roomName =
|
||||
_collapseWhitespace(lesson.rooms.firstOrNull) ?? 'Unbekannt';
|
||||
final teacherName =
|
||||
_teacherLabel(lesson.teachers.firstOrNull) ?? 'Unbekannt';
|
||||
return '$roomName\n$teacherName';
|
||||
collapseWhitespace(lesson.rooms.firstOrNull) ?? 'Unbekannt';
|
||||
// Klassenlose Einträge (Aufsichten etc.) fallen auf den Lehrer zurück.
|
||||
final secondLine =
|
||||
(showClassInsteadOfTeacher ? lesson.classLabel : null) ??
|
||||
_teacherLabel(lesson.teachers.firstOrNull) ??
|
||||
'Unbekannt';
|
||||
return '$roomName\n$secondLine';
|
||||
}
|
||||
|
||||
/// Backend serves teachers with their full display name ("Stefan Müller"),
|
||||
@@ -245,27 +254,11 @@ class TimetableAppointmentFactory {
|
||||
/// overview; the detail sheet still renders the full name as a subtitle.
|
||||
static String? _teacherLabel(McTimetableTeacher? teacher) {
|
||||
if (teacher == null) return null;
|
||||
final display = _collapseWhitespace(teacher.displayName);
|
||||
final display = collapseWhitespace(teacher.displayName);
|
||||
if (display != null && display.isNotEmpty) {
|
||||
final parts = display.split(' ');
|
||||
return parts.isEmpty ? display : parts.last;
|
||||
}
|
||||
return _collapseWhitespace(teacher.shortName);
|
||||
}
|
||||
|
||||
/// Collapses any line-break or whitespace run to a single space and trims.
|
||||
/// Returns null when input is null or fully whitespace. Webuntis sometimes
|
||||
/// returns multi-line room names like "A30\n4" — this normalizes those so
|
||||
/// the tile renders the room on a single line.
|
||||
static String? _collapseWhitespace(String? s) {
|
||||
if (s == null) return null;
|
||||
final cleaned = s
|
||||
.replaceAll('\r\n', ' ')
|
||||
.replaceAll('\n', ' ')
|
||||
.replaceAll('\r', ' ')
|
||||
.replaceAll('\t', ' ')
|
||||
.replaceAll(RegExp(r'\s+'), ' ')
|
||||
.trim();
|
||||
return cleaned.isEmpty ? null : cleaned;
|
||||
return collapseWhitespace(teacher.shortName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,9 +101,8 @@ class _TimetableState extends State<Timetable> {
|
||||
final loadableState = context.watch<TimetableBloc>().state;
|
||||
final innerState = loadableState.data;
|
||||
final atToday = innerState != null && _isOnInitialWeek(innerState);
|
||||
final canViewForeign = context
|
||||
.watch<CapabilitiesCubit>()
|
||||
.canViewForeignTimetables;
|
||||
final capabilities = context.watch<CapabilitiesCubit>();
|
||||
final canViewForeign = capabilities.canViewForeignTimetables;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// Der Kalender scrollt nicht (nur ziehen/reloaden), aber seine internen
|
||||
@@ -166,6 +165,7 @@ class _TimetableState extends State<Timetable> {
|
||||
),
|
||||
onCreateEvent: _onCreateEventAt,
|
||||
customEvents: state.customEvents?.events ?? const [],
|
||||
showClassInsteadOfTeacher: capabilities.isTeacher,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -217,6 +217,8 @@ class _TimetableState extends State<Timetable> {
|
||||
onAppointmentTap: (apt) =>
|
||||
AppointmentDetailsDispatcher.show(context, state, apt),
|
||||
customEvents: const [],
|
||||
showClassInsteadOfTeacher:
|
||||
selected.type == TimetableElementType.teacher,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -28,6 +28,10 @@ class TimetableCalendarView extends StatefulWidget {
|
||||
final void Function(DateTime start, DateTime end)? onCreateEvent;
|
||||
final List<CustomTimetableEvent> customEvents;
|
||||
|
||||
/// True for teacher plans — tiles then show the class instead of the
|
||||
/// teacher name (see [TimetableAppointmentFactory.showClassInsteadOfTeacher]).
|
||||
final bool showClassInsteadOfTeacher;
|
||||
|
||||
const TimetableCalendarView({
|
||||
super.key,
|
||||
required this.state,
|
||||
@@ -35,6 +39,7 @@ class TimetableCalendarView extends StatefulWidget {
|
||||
required this.onAppointmentTap,
|
||||
this.onCreateEvent,
|
||||
this.customEvents = const [],
|
||||
this.showClassInsteadOfTeacher = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -46,9 +51,9 @@ class TimetableCalendarViewState extends State<TimetableCalendarView> {
|
||||
GlobalKey<CustomWorkWeekCalendarState>();
|
||||
|
||||
List<Appointment>? _cachedAppointments;
|
||||
int? _lastDataVersion;
|
||||
TimetableSettings? _lastTimetableSettings;
|
||||
List<CustomTimetableEvent>? _lastCustomEvents;
|
||||
// TimetableSettings and List define no `==`, so record equality degrades to
|
||||
// the same identity checks the cache always used.
|
||||
(int, TimetableSettings, List<CustomTimetableEvent>, bool)? _cacheKey;
|
||||
|
||||
DateTime _initialDisplayDate() => DateTime.now().addDays(2);
|
||||
|
||||
@@ -63,15 +68,16 @@ class TimetableCalendarViewState extends State<TimetableCalendarView> {
|
||||
.watch<SettingsCubit>()
|
||||
.val()
|
||||
.timetableSettings;
|
||||
if (_cachedAppointments != null &&
|
||||
_lastDataVersion == state.dataVersion &&
|
||||
identical(_lastTimetableSettings, timetableSettings) &&
|
||||
identical(_lastCustomEvents, widget.customEvents)) {
|
||||
final key = (
|
||||
state.dataVersion,
|
||||
timetableSettings,
|
||||
widget.customEvents,
|
||||
widget.showClassInsteadOfTeacher,
|
||||
);
|
||||
if (_cachedAppointments != null && _cacheKey == key) {
|
||||
return _cachedAppointments!;
|
||||
}
|
||||
_lastDataVersion = state.dataVersion;
|
||||
_lastTimetableSettings = timetableSettings;
|
||||
_lastCustomEvents = widget.customEvents;
|
||||
_cacheKey = key;
|
||||
|
||||
return _cachedAppointments = TimetableAppointmentFactory(
|
||||
lessons: state.getAllKnownLessons().toList(),
|
||||
@@ -80,6 +86,7 @@ class TimetableCalendarViewState extends State<TimetableCalendarView> {
|
||||
holidays: state.schoolHolidays?.result ?? const [],
|
||||
settings: timetableSettings,
|
||||
now: DateTime.now(),
|
||||
showClassInsteadOfTeacher: widget.showClassInsteadOfTeacher,
|
||||
).build();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user