fixed lesson merging mutation, improved overlap detection, and implemented priority-based lane assignment with tablet support
This commit is contained in:
@@ -7,6 +7,7 @@ import 'package:intl/intl.dart';
|
||||
import 'package:rrule/rrule.dart';
|
||||
import 'package:syncfusion_flutter_calendar/calendar.dart';
|
||||
|
||||
import '../data/arbitrary_appointment.dart';
|
||||
import '../data/calendar_layout.dart';
|
||||
import '../data/lesson_period_schedule.dart';
|
||||
import 'appointment_tile.dart';
|
||||
@@ -853,7 +854,8 @@ class _DayColumn extends StatelessWidget {
|
||||
final dayRegions = _expandRegionsForDay(timeRegions, date);
|
||||
final isToday = _isSameDay(date, today);
|
||||
|
||||
final laidOut = _assignLanes(dayAppointments);
|
||||
final isTablet = MediaQuery.of(context).size.shortestSide >= 600;
|
||||
final laidOut = _assignLanes(dayAppointments, maxLanes: isTablet ? 3 : 2);
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
@@ -1209,11 +1211,6 @@ class _PeriodLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum number of cells shown side by side in a single time slot. When a
|
||||
/// cluster needs more lanes than this, the first appointment (by start time)
|
||||
/// keeps lane 0 and the rest are collapsed into a single "+N" overflow cell
|
||||
/// in lane 1.
|
||||
const int _kMaxVisibleCells = 2;
|
||||
|
||||
class _OverflowTile extends StatelessWidget {
|
||||
final int count;
|
||||
@@ -1324,22 +1321,40 @@ class _LaidOutOverflow extends _LaidOutCell {
|
||||
_LaidOutOverflow(this.appointments, this.lane, this.laneCount, this.startTime, this.endTime);
|
||||
}
|
||||
|
||||
/// Horizontal ordering rank for parallel appointments. Lower = further left.
|
||||
/// User-owned custom events sit on the leftmost lane, cancelled lessons after
|
||||
/// them, every other lesson last. Only used as a tiebreaker — the greedy lane
|
||||
/// assignment still has to honor actual time-overlap constraints, so events
|
||||
/// that start later can't jump left of events that started earlier and are
|
||||
/// still occupying that lane.
|
||||
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;
|
||||
}
|
||||
|
||||
/// Assigns each appointment a lane index using a greedy sweep, then collapses
|
||||
/// clusters that exceed [_kMaxVisibleCells] into 1 visible appointment + 1
|
||||
/// overflow cell side by side.
|
||||
/// clusters that exceed [maxLanes] into `(maxLanes - 1)` visible appointments
|
||||
/// + one trailing overflow cell.
|
||||
///
|
||||
/// Greedy sweep:
|
||||
/// 1. Sort by `startTime` ascending, `endTime` descending on ties.
|
||||
/// 1. Sort by `startTime` ascending, then [_appointmentPriority] (custom →
|
||||
/// cancelled → other) so parallel events land in the requested left-to-
|
||||
/// right order, then `endTime` descending as a final tiebreaker.
|
||||
/// 2. Walk the list, placing each appointment in the lowest-index lane that
|
||||
/// 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) {
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -1381,17 +1396,29 @@ List<_LaidOutCell> _assignLanes(List<Appointment> appts) {
|
||||
final laneCount =
|
||||
cluster.fold<int>(0, (m, e) => e.lane + 1 > m ? e.lane + 1 : m);
|
||||
|
||||
if (laneCount <= _kMaxVisibleCells) {
|
||||
if (laneCount <= maxLanes) {
|
||||
for (final entry in cluster) {
|
||||
result.add(_LaidOutAppointment(entry.apt, entry.lane, laneCount));
|
||||
}
|
||||
} else {
|
||||
// 3+ parallel appointments: keep the earliest, collapse the rest.
|
||||
final byStart = [...cluster.map((e) => e.apt)]
|
||||
..sort((a, b) => a.startTime.compareTo(b.startTime));
|
||||
result.add(_LaidOutAppointment(byStart[0], 0, _kMaxVisibleCells));
|
||||
// Too many parallel appointments: keep the highest-priority
|
||||
// (maxLanes - 1) and collapse the rest into a single overflow cell in
|
||||
// the trailing lane. Sorting by priority first means custom and
|
||||
// cancelled lessons stay visible when the cluster has to be trimmed,
|
||||
// matching the requested left-to-right order in the visible lanes.
|
||||
final visibleCount = maxLanes - 1;
|
||||
final byPriority = [...cluster.map((e) => e.apt)]
|
||||
..sort((a, b) {
|
||||
final p = _appointmentPriority(a).compareTo(_appointmentPriority(b));
|
||||
if (p != 0) return p;
|
||||
return a.startTime.compareTo(b.startTime);
|
||||
});
|
||||
|
||||
final overflow = byStart.sublist(1);
|
||||
for (var i = 0; i < visibleCount; i++) {
|
||||
result.add(_LaidOutAppointment(byPriority[i], i, maxLanes));
|
||||
}
|
||||
|
||||
final overflow = byPriority.sublist(visibleCount);
|
||||
var earliest = overflow.first.startTime;
|
||||
var latest = overflow.first.endTime;
|
||||
for (final a in overflow.skip(1)) {
|
||||
@@ -1399,7 +1426,7 @@ List<_LaidOutCell> _assignLanes(List<Appointment> appts) {
|
||||
if (a.endTime.isAfter(latest)) latest = a.endTime;
|
||||
}
|
||||
result.add(_LaidOutOverflow(
|
||||
overflow, _kMaxVisibleCells - 1, _kMaxVisibleCells, earliest, latest));
|
||||
overflow, maxLanes - 1, maxLanes, earliest, latest));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user