getCalendar method
override
Fetches academic calendar events within a date range.
Returns a list of calendar events (e.g., holidays, exam periods,
registration deadlines) between startDate and endDate inclusive.
Requires an active portal session (call login first).
Implementation
@override
Future<List<CalendarEventDto>> getCalendar(
DateTime startDate,
DateTime endDate,
) async {
final formatter = DateFormat('yyyy/MM/dd');
final response = await _portalDio.get(
'calModeApp.do',
queryParameters: {
'startDate': formatter.format(startDate),
'endDate': formatter.format(endDate),
},
);
final List<dynamic> events = jsonDecode(response.data);
String? normalizeEmpty(String? value) =>
value?.isNotEmpty == true ? value : null;
// The portal returns proper Unix epoch ms (e.g. 1753977600000 ↔
// 2025-08-01 00:00 +08:00). Naively decoding via the device's local
// timezone would shift the displayed date when the user is outside
// Taipei — Aug 1 would render as Jul 31 in London. We instead build
// a UTC DateTime, add 8h, and copy its wall-clock fields into a local
// DateTime so the result reads as Taipei time regardless of the
// device's offset (and DST).
DateTime taipeiWallClock(int ms) {
final taipei = DateTime.fromMillisecondsSinceEpoch(
ms,
isUtc: true,
).add(const Duration(hours: 8));
return DateTime(
taipei.year,
taipei.month,
taipei.day,
taipei.hour,
taipei.minute,
taipei.second,
taipei.millisecond,
);
}
return events
.where(
// Filter out weekend markers
(e) => e['isHoliday'] != '1',
)
.map<CalendarEventDto>(
(e) => (
id: e['id'],
start: taipeiWallClock(e['calStart']),
end: taipeiWallClock(e['calEnd']),
allDay: e['allDay'] == '1',
title: normalizeEmpty(e['calTitle']),
place: normalizeEmpty(e['calPlace']),
content: normalizeEmpty(e['calContent']),
ownerName: normalizeEmpty(e['ownerName']),
creatorName: normalizeEmpty(e['creatorName']),
),
)
.toList();
}