dart format

This commit is contained in:
2026-05-08 20:12:40 +02:00
parent 9e139b5704
commit 3b8da1d3d6
295 changed files with 6404 additions and 4161 deletions
@@ -8,9 +8,9 @@ sealed class ArbitraryAppointment {
required T Function(GetTimetableResponseObject lesson) webuntis,
required T Function(CustomTimetableEvent event) custom,
}) => switch (this) {
WebuntisAppointment(:final lesson) => webuntis(lesson),
CustomAppointment(:final event) => custom(event),
};
WebuntisAppointment(:final lesson) => webuntis(lesson),
CustomAppointment(:final event) => custom(event),
};
}
class WebuntisAppointment extends ArbitraryAppointment {
@@ -43,24 +43,28 @@ List<BoundRegion> expandRegionsForDay(List<TimeRegion> regions, DateTime day) {
final result = <BoundRegion>[];
final dayStart = DateTime(day.year, day.month, day.day);
for (final region in regions) {
final isRecurringDaily = region.recurrenceRule != null &&
final isRecurringDaily =
region.recurrenceRule != null &&
region.recurrenceRule!.toUpperCase().contains('FREQ=DAILY');
if (isRecurringDaily) {
final start = dayStart.add(Duration(
hours: region.startTime.hour,
minutes: region.startTime.minute,
));
final end = dayStart.add(Duration(
hours: region.endTime.hour,
minutes: region.endTime.minute,
));
final start = dayStart.add(
Duration(
hours: region.startTime.hour,
minutes: region.startTime.minute,
),
);
final end = dayStart.add(
Duration(hours: region.endTime.hour, minutes: region.endTime.minute),
);
result.add(BoundRegion(region: region, start: start, end: end));
} else if (region.startTime.isSameDay(day)) {
result.add(BoundRegion(
region: region,
start: region.startTime,
end: region.endTime,
));
result.add(
BoundRegion(
region: region,
start: region.startTime,
end: region.endTime,
),
);
}
}
return result;
@@ -73,8 +77,10 @@ List<BoundRegion> expandRegionsForDay(List<TimeRegion> regions, DateTime day) {
/// [kCalendarStartHour] or end after [kCalendarEndHour]). The outside bucket
/// is rendered as chips above the grid.
({List<List<Appointment>> inside, List<List<Appointment>> outside})
partitionAppointmentsForWeek(
List<Appointment> appointments, DateTime weekStart) {
partitionAppointmentsForWeek(
List<Appointment> appointments,
DateTime weekStart,
) {
final inside = List<List<Appointment>>.generate(5, (_) => <Appointment>[]);
final outside = List<List<Appointment>>.generate(5, (_) => <Appointment>[]);
final weekEnd = weekStart.add(const Duration(days: 5));
@@ -104,12 +110,19 @@ List<BoundRegion> expandRegionsForDay(List<TimeRegion> regions, DateTime day) {
if (!occUtc.isBefore(weekEndUtc)) break;
if (occUtc.isBefore(weekStartUtc)) continue;
final occLocal = occUtc.toLocal();
final idx = DateTime(occLocal.year, occLocal.month, occLocal.day)
.difference(weekStart)
.inDays;
final idx = DateTime(
occLocal.year,
occLocal.month,
occLocal.day,
).difference(weekStart).inDays;
if (idx < 0 || idx >= 5) continue;
final newStart = DateTime(occLocal.year, occLocal.month, occLocal.day,
a.startTime.hour, a.startTime.minute);
final newStart = DateTime(
occLocal.year,
occLocal.month,
occLocal.day,
a.startTime.hour,
a.startTime.minute,
);
place(
idx,
Appointment(
@@ -150,8 +163,7 @@ class PeriodLayout {
double _h(LessonPeriod p) => p.isBreak ? breakHeight : lessonHeight;
double get totalHeight =>
periods.fold<double>(0, (sum, p) => sum + _h(p));
double get totalHeight => periods.fold<double>(0, (sum, p) => sum + _h(p));
double topOf(LessonPeriod period) {
var y = 0.0;
@@ -241,7 +253,13 @@ class LaidOutOverflow extends LaidOutCell {
final DateTime startTime;
@override
final DateTime endTime;
LaidOutOverflow(this.appointments, this.lane, this.laneCount, this.startTime, this.endTime);
LaidOutOverflow(
this.appointments,
this.lane,
this.laneCount,
this.startTime,
this.endTime,
);
}
/// Horizontal ordering rank for parallel appointments. Lower = further left.
@@ -269,17 +287,21 @@ int _appointmentPriority(Appointment a) {
/// is free at its `startTime`. When no lane is free, open a new one.
/// 3. A cluster ends as soon as every active lane's end is at or before the
/// next appointment's start.
List<LaidOutCell> assignLanes(List<Appointment> appts, {required int maxLanes}) {
List<LaidOutCell> assignLanes(
List<Appointment> appts, {
required int maxLanes,
}) {
assert(maxLanes >= 2, 'maxLanes must reserve at least one slot for overflow');
if (appts.isEmpty) return const <LaidOutCell>[];
final sorted = [...appts]..sort((a, b) {
final c = a.startTime.compareTo(b.startTime);
if (c != 0) return c;
final p = _appointmentPriority(a).compareTo(_appointmentPriority(b));
if (p != 0) return p;
return b.endTime.compareTo(a.endTime);
});
final sorted = [...appts]
..sort((a, b) {
final c = a.startTime.compareTo(b.startTime);
if (c != 0) return c;
final p = _appointmentPriority(a).compareTo(_appointmentPriority(b));
if (p != 0) return p;
return b.endTime.compareTo(a.endTime);
});
// Phase 1: greedy lane assignment, grouped by cluster.
final clusters = <List<({Appointment apt, int lane})>>[];
@@ -288,7 +310,8 @@ List<LaidOutCell> assignLanes(List<Appointment> appts, {required int maxLanes})
for (final apt in sorted) {
final allFree =
laneEnds.isNotEmpty && laneEnds.every((end) => !end.isAfter(apt.startTime));
laneEnds.isNotEmpty &&
laneEnds.every((end) => !end.isAfter(apt.startTime));
if (allFree) {
clusters.add(current);
current = <({Appointment apt, int lane})>[];
@@ -316,8 +339,10 @@ List<LaidOutCell> assignLanes(List<Appointment> appts, {required int maxLanes})
// Phase 2: emit cells per cluster, collapsing if too wide.
final result = <LaidOutCell>[];
for (final cluster in clusters) {
final laneCount =
cluster.fold<int>(0, (m, e) => e.lane + 1 > m ? e.lane + 1 : m);
final laneCount = cluster.fold<int>(
0,
(m, e) => e.lane + 1 > m ? e.lane + 1 : m,
);
if (laneCount <= maxLanes) {
for (final entry in cluster) {
@@ -348,8 +373,9 @@ List<LaidOutCell> assignLanes(List<Appointment> appts, {required int maxLanes})
if (a.startTime.isBefore(earliest)) earliest = a.startTime;
if (a.endTime.isAfter(latest)) latest = a.endTime;
}
result.add(LaidOutOverflow(
overflow, maxLanes - 1, maxLanes, earliest, latest));
result.add(
LaidOutOverflow(overflow, maxLanes - 1, maxLanes, earliest, latest),
);
}
}
return result;
@@ -17,8 +17,8 @@ class LessonPeriod {
});
Duration get duration => Duration(
minutes: (end.hour * 60 + end.minute) - (start.hour * 60 + start.minute),
);
minutes: (end.hour * 60 + end.minute) - (start.hour * 60 + start.minute),
);
int get _startMinutes => start.hour * 60 + start.minute;
}
@@ -31,39 +31,94 @@ class LessonPeriodSchedule {
static LessonPeriodSchedule? fromApi(GetTimegridUnitsResponse response) {
final canonical = response.result.firstWhere(
(d) => d.day == 1,
orElse: () => response.result.isNotEmpty ? response.result.first : GetTimegridUnitsResponseDay(0, []),
orElse: () => response.result.isNotEmpty
? response.result.first
: GetTimegridUnitsResponseDay(0, []),
);
if (canonical.timeUnits.isEmpty) return null;
final periods = canonical.timeUnits
.map((u) => LessonPeriod(
name: u.name,
start: _fromHHMM(u.startTime),
end: _fromHHMM(u.endTime),
))
.toList()
..sort((a, b) => a._startMinutes.compareTo(b._startMinutes));
final periods =
canonical.timeUnits
.map(
(u) => LessonPeriod(
name: u.name,
start: _fromHHMM(u.startTime),
end: _fromHHMM(u.endTime),
),
)
.toList()
..sort((a, b) => a._startMinutes.compareTo(b._startMinutes));
return LessonPeriodSchedule(periods);
}
static LessonPeriodSchedule fallback() => const LessonPeriodSchedule([
LessonPeriod(name: '0', start: TimeOfDay(hour: 7, minute: 10), end: TimeOfDay(hour: 7, minute: 53)),
LessonPeriod(name: '1', start: TimeOfDay(hour: 7, minute: 55), end: TimeOfDay(hour: 8, minute: 40)),
LessonPeriod(name: '2', start: TimeOfDay(hour: 8, minute: 40), end: TimeOfDay(hour: 9, minute: 25)),
LessonPeriod(name: '3', start: TimeOfDay(hour: 9, minute: 30), end: TimeOfDay(hour: 10, minute: 15)),
LessonPeriod(name: '4', start: TimeOfDay(hour: 10, minute: 35), end: TimeOfDay(hour: 11, minute: 20)),
LessonPeriod(name: '5', start: TimeOfDay(hour: 11, minute: 25), end: TimeOfDay(hour: 12, minute: 10)),
LessonPeriod(name: '6', start: TimeOfDay(hour: 12, minute: 15), end: TimeOfDay(hour: 13, minute: 0)),
LessonPeriod(name: '7', start: TimeOfDay(hour: 13, minute: 5), end: TimeOfDay(hour: 13, minute: 50)),
LessonPeriod(name: '8', start: TimeOfDay(hour: 14, minute: 5), end: TimeOfDay(hour: 14, minute: 50)),
LessonPeriod(name: '9', start: TimeOfDay(hour: 14, minute: 50), end: TimeOfDay(hour: 15, minute: 35)),
LessonPeriod(name: '10', start: TimeOfDay(hour: 15, minute: 40), end: TimeOfDay(hour: 16, minute: 25)),
LessonPeriod(name: '11', start: TimeOfDay(hour: 16, minute: 25), end: TimeOfDay(hour: 17, minute: 10)),
]);
LessonPeriod(
name: '0',
start: TimeOfDay(hour: 7, minute: 10),
end: TimeOfDay(hour: 7, minute: 53),
),
LessonPeriod(
name: '1',
start: TimeOfDay(hour: 7, minute: 55),
end: TimeOfDay(hour: 8, minute: 40),
),
LessonPeriod(
name: '2',
start: TimeOfDay(hour: 8, minute: 40),
end: TimeOfDay(hour: 9, minute: 25),
),
LessonPeriod(
name: '3',
start: TimeOfDay(hour: 9, minute: 30),
end: TimeOfDay(hour: 10, minute: 15),
),
LessonPeriod(
name: '4',
start: TimeOfDay(hour: 10, minute: 35),
end: TimeOfDay(hour: 11, minute: 20),
),
LessonPeriod(
name: '5',
start: TimeOfDay(hour: 11, minute: 25),
end: TimeOfDay(hour: 12, minute: 10),
),
LessonPeriod(
name: '6',
start: TimeOfDay(hour: 12, minute: 15),
end: TimeOfDay(hour: 13, minute: 0),
),
LessonPeriod(
name: '7',
start: TimeOfDay(hour: 13, minute: 5),
end: TimeOfDay(hour: 13, minute: 50),
),
LessonPeriod(
name: '8',
start: TimeOfDay(hour: 14, minute: 5),
end: TimeOfDay(hour: 14, minute: 50),
),
LessonPeriod(
name: '9',
start: TimeOfDay(hour: 14, minute: 50),
end: TimeOfDay(hour: 15, minute: 35),
),
LessonPeriod(
name: '10',
start: TimeOfDay(hour: 15, minute: 40),
end: TimeOfDay(hour: 16, minute: 25),
),
LessonPeriod(
name: '11',
start: TimeOfDay(hour: 16, minute: 25),
end: TimeOfDay(hour: 17, minute: 10),
),
]);
static LessonPeriodSchedule fromState(TimetableState state) {
final fromApi = state.timegrid != null ? LessonPeriodSchedule.fromApi(state.timegrid!) : null;
final fromApi = state.timegrid != null
? LessonPeriodSchedule.fromApi(state.timegrid!)
: null;
return (fromApi ?? fallback()).withSyntheticBreaks();
}
@@ -74,21 +129,22 @@ class LessonPeriodSchedule {
result.add(current);
if (i + 1 >= periods.length) continue;
final next = periods[i + 1];
final gapMinutes = next._startMinutes - (current.end.hour * 60 + current.end.minute);
final gapMinutes =
next._startMinutes - (current.end.hour * 60 + current.end.minute);
if (gapMinutes >= 10) {
result.add(LessonPeriod(
name: 'Pause',
start: current.end,
end: next.start,
isBreak: true,
));
result.add(
LessonPeriod(
name: 'Pause',
start: current.end,
end: next.start,
isBreak: true,
),
);
}
}
return LessonPeriodSchedule(result);
}
static TimeOfDay _fromHHMM(int hhmm) => TimeOfDay(
hour: hhmm ~/ 100,
minute: hhmm % 100,
);
static TimeOfDay _fromHHMM(int hhmm) =>
TimeOfDay(hour: hhmm ~/ 100, minute: hhmm % 100);
}
@@ -20,10 +20,17 @@ class LessonStatusClassifier {
}) {
if (lesson.code == 'cancelled') return LessonStatus.cancelled;
if (isEvent) return LessonStatus.event;
if (lesson.code == 'irregular' || (lesson.te.isNotEmpty && lesson.te.first.id == 0)) return LessonStatus.irregular;
if (lesson.te.any((t) => t.orgname != null)) return LessonStatus.teacherChanged;
if (lesson.code == 'irregular' ||
(lesson.te.isNotEmpty && lesson.te.first.id == 0)) {
return LessonStatus.irregular;
}
if (lesson.te.any((t) => t.orgname != null)) {
return LessonStatus.teacherChanged;
}
if (endTime.isBefore(now)) return LessonStatus.past;
if (startTime.isBefore(now) && endTime.isAfter(now)) return LessonStatus.ongoing;
if (startTime.isBefore(now) && endTime.isAfter(now)) {
return LessonStatus.ongoing;
}
return LessonStatus.regular;
}
}
@@ -31,7 +31,9 @@ class TimetableAppointmentFactory {
});
List<Appointment> build() {
final source = settings.connectDoubleLessons ? _mergeAdjacentLessons(lessons) : lessons;
final source = settings.connectDoubleLessons
? _mergeAdjacentLessons(lessons)
: lessons;
return [
...source.map(_lessonToAppointment),
...customEvents.map(_customEventToAppointment),
@@ -42,7 +44,9 @@ class TimetableAppointmentFactory {
try {
final startTime = WebuntisTime.parse(lesson.date, lesson.startTime);
final endTime = WebuntisTime.parse(lesson.date, lesson.endTime);
final subject = subjects.result.firstWhereOrNull((s) => s.id == lesson.su.firstOrNull?.id);
final subject = subjects.result.firstWhereOrNull(
(s) => s.id == lesson.su.firstOrNull?.id,
);
final status = LessonStatusClassifier.classify(
lesson,
startTime,
@@ -81,16 +85,26 @@ class TimetableAppointmentFactory {
id: CustomAppointment(event),
startTime: event.startDate,
endTime: allDay
? DateTime(event.startDate.year, event.startDate.month, event.startDate.day, 23, 59)
? DateTime(
event.startDate.year,
event.startDate.month,
event.startDate.day,
23,
59,
)
: event.endDate,
isAllDay: allDay,
// Preserve user-entered newlines in descriptions; the tile soft-wraps to
// fill the available height. For lessons we still collapse whitespace
// so room/teacher stay on one line each.
location: event.description.trim().isEmpty ? null : event.description.trim(),
location: event.description.trim().isEmpty
? null
: event.description.trim(),
subject: _collapseWhitespace(event.title) ?? event.title,
recurrenceRule: event.rrule,
color: TimetableColors.getColorFromString(event.color ?? TimetableColors.defaultColor.name),
color: TimetableColors.getColorFromString(
event.color ?? TimetableColors.defaultColor.name,
),
startTimeZone: '',
endTimeZone: '',
);
@@ -114,7 +128,10 @@ class TimetableAppointmentFactory {
e.second == 0;
}
String _subjectName(GetTimetableResponseObject lesson, GetSubjectsResponseObject? subject) {
String _subjectName(
GetTimetableResponseObject lesson,
GetSubjectsResponseObject? subject,
) {
if (subject == null) return 'Event';
final name = switch (settings.timetableNameMode) {
TimetableNameMode.name => subject.name,
@@ -125,10 +142,15 @@ class TimetableAppointmentFactory {
}
String _locationLabel(GetTimetableResponseObject lesson) {
final roomName = _collapseWhitespace(
rooms.result.firstWhereOrNull((r) => r.id == lesson.ro.firstOrNull?.id)?.name) ??
final roomName =
_collapseWhitespace(
rooms.result
.firstWhereOrNull((r) => r.id == lesson.ro.firstOrNull?.id)
?.name,
) ??
'Unbekannt';
final teacherName = _collapseWhitespace(lesson.te.firstOrNull?.longname) ?? 'Unbekannt';
final teacherName =
_collapseWhitespace(lesson.te.firstOrNull?.longname) ?? 'Unbekannt';
return '$roomName\n$teacherName';
}
@@ -161,8 +183,13 @@ class TimetableAppointmentFactory {
}) {
if (input.isEmpty) return const [];
final sorted = [...input]..sort((a, b) =>
WebuntisTime.parse(a.date, a.startTime).compareTo(WebuntisTime.parse(b.date, b.startTime)));
final sorted = [...input]
..sort(
(a, b) => WebuntisTime.parse(
a.date,
a.startTime,
).compareTo(WebuntisTime.parse(b.date, b.startTime)),
);
final merged = <GetTimetableResponseObject>[];
for (final current in sorted) {
@@ -180,10 +207,16 @@ class TimetableAppointmentFactory {
static GetTimetableResponseObject _copyLesson(GetTimetableResponseObject l) =>
GetTimetableResponseObject.fromJson(l.toJson());
static bool _canMerge(GetTimetableResponseObject a, GetTimetableResponseObject b, Duration maxGap) {
static bool _canMerge(
GetTimetableResponseObject a,
GetTimetableResponseObject b,
Duration maxGap,
) {
final aSubject = a.su.firstOrNull?.id;
final bSubject = b.su.firstOrNull?.id;
if (aSubject == null || bSubject == null || aSubject != bSubject) return false;
if (aSubject == null || bSubject == null || aSubject != bSubject) {
return false;
}
if (a.ro.firstOrNull?.id != b.ro.firstOrNull?.id) return false;
if (a.te.firstOrNull?.id != b.te.firstOrNull?.id) return false;
if (a.code != b.code) return false;
@@ -193,7 +226,10 @@ class TimetableAppointmentFactory {
// overlap in time would silently collapse into one — and because the
// merge sets `previous.endTime = current.endTime`, an overlapping merge
// can even truncate the earlier lesson.
final gap = WebuntisTime.parse(b.date, b.startTime).difference(WebuntisTime.parse(a.date, a.endTime));
final gap = WebuntisTime.parse(
b.date,
b.startTime,
).difference(WebuntisTime.parse(a.date, a.endTime));
return !gap.isNegative && gap <= maxGap;
}
}
@@ -8,11 +8,20 @@ class TimetableNameModes {
static DropdownDisplay getDisplayOptions(TimetableNameMode mode) {
switch (mode) {
case TimetableNameMode.name:
return DropdownDisplay(icon: Icons.device_unknown_outlined, displayName: 'Name');
return DropdownDisplay(
icon: Icons.device_unknown_outlined,
displayName: 'Name',
);
case TimetableNameMode.longName:
return DropdownDisplay(icon: Icons.perm_device_info_outlined, displayName: 'Langname');
return DropdownDisplay(
icon: Icons.perm_device_info_outlined,
displayName: 'Langname',
);
case TimetableNameMode.alternateName:
return DropdownDisplay(icon: Icons.on_device_training_outlined, displayName: 'Kurzform');
return DropdownDisplay(
icon: Icons.on_device_training_outlined,
displayName: 'Kurzform',
);
}
}
}
@@ -5,7 +5,9 @@ class WebuntisTime {
static DateTime parse(int date, int time) {
final timeString = time.toString().padLeft(4, '0');
return DateTime.parse('$date ${timeString.substring(0, 2)}:${timeString.substring(2, 4)}');
return DateTime.parse(
'$date ${timeString.substring(0, 2)}:${timeString.substring(2, 4)}',
);
}
static int formatDate(DateTime date) => int.parse(_dateFormat.format(date));