Skip to content

Commit a74c3fe

Browse files
committed
Add repeated calendar items
1 parent b50b929 commit a74c3fe

21 files changed

Lines changed: 1170 additions & 464 deletions

api/lib/converters/ical.dart

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,17 @@ class _RRule {
99
final int interval;
1010
final int count;
1111
final DateTime? until;
12-
_RRule(this.repeatType, this.interval, this.count, this.until);
12+
final List<int> byWeekDays;
13+
final List<int> byMonthDays;
14+
15+
_RRule(
16+
this.repeatType,
17+
this.interval,
18+
this.count,
19+
this.until, {
20+
this.byWeekDays = const [],
21+
this.byMonthDays = const [],
22+
});
1323
}
1424

1525
class ICalConverter {
@@ -100,7 +110,7 @@ class ICalConverter {
100110
status: c.status,
101111
repeatType: rrule.repeatType,
102112
interval: rrule.interval,
103-
variation: 0,
113+
variation: _variationFromRRule(rrule),
104114
count: rrule.count,
105115
until: rrule.until,
106116
);
@@ -197,6 +207,8 @@ class ICalConverter {
197207
int interval = 1;
198208
int count = 0;
199209
DateTime? until;
210+
var byWeekDays = <int>[];
211+
var byMonthDays = <int>[];
200212

201213
final parts = value.split(';');
202214
for (final part in parts) {
@@ -218,9 +230,70 @@ class ICalConverter {
218230
count = int.tryParse(v) ?? 0;
219231
} else if (k == 'UNTIL') {
220232
until = _parseDateTime(v);
233+
} else if (k == 'BYDAY') {
234+
byWeekDays = _parseByDay(v);
235+
} else if (k == 'BYMONTHDAY') {
236+
byMonthDays = _parseByMonthDay(v);
221237
}
222238
}
223-
return _RRule(type, interval, count, until);
239+
return _RRule(
240+
type,
241+
interval,
242+
count,
243+
until,
244+
byWeekDays: byWeekDays,
245+
byMonthDays: byMonthDays,
246+
);
247+
}
248+
249+
int _variationFromRRule(_RRule rule) {
250+
switch (rule.repeatType) {
251+
case RepeatType.weekly:
252+
return RepeatingCalendarItem.encodeWeeklyWeekdays(rule.byWeekDays);
253+
case RepeatType.monthly:
254+
return RepeatingCalendarItem.encodeMonthlyMonthDays(rule.byMonthDays);
255+
case RepeatType.daily:
256+
case RepeatType.yearly:
257+
return 0;
258+
}
259+
}
260+
261+
List<int> _parseByDay(String value) {
262+
final weekdays = <int>[];
263+
for (final part in value.split(',')) {
264+
final token = part.trim().toUpperCase();
265+
if (token.length < 2) continue;
266+
final dayCode = token.substring(token.length - 2);
267+
final weekday = _weekdayFromIcs(dayCode);
268+
if (weekday != null) {
269+
weekdays.add(weekday);
270+
}
271+
}
272+
return weekdays;
273+
}
274+
275+
List<int> _parseByMonthDay(String value) {
276+
final days = <int>[];
277+
for (final part in value.split(',')) {
278+
final day = int.tryParse(part.trim());
279+
if (day != null && day >= 1 && day <= 31) {
280+
days.add(day);
281+
}
282+
}
283+
return days;
284+
}
285+
286+
int? _weekdayFromIcs(String day) {
287+
return switch (day) {
288+
'MO' => DateTime.monday,
289+
'TU' => DateTime.tuesday,
290+
'WE' => DateTime.wednesday,
291+
'TH' => DateTime.thursday,
292+
'FR' => DateTime.friday,
293+
'SA' => DateTime.saturday,
294+
'SU' => DateTime.sunday,
295+
_ => null,
296+
};
224297
}
225298

226299
EventStatus _parseEventStatus(String value) {
@@ -254,12 +327,49 @@ class ICalConverter {
254327
if (item.location.isNotEmpty) 'LOCATION:${_escape(item.location)}',
255328
if (item.start != null) 'DTSTART:${_formatDateTime(item.start!.toUtc())}',
256329
if (item.end != null) 'DTEND:${_formatDateTime(item.end!.toUtc())}',
257-
if (item is RepeatingCalendarItem)
258-
'RRULE:FREQ=${_formatRepeatType(item.repeatType)}${item.interval > 1 ? ';INTERVAL=${item.interval}' : ''}${item.count > 0 ? ';COUNT=${item.count}' : ''}${item.until != null ? ';UNTIL=${_formatDateTime(item.until!.toUtc())}' : ''}',
330+
if (item is RepeatingCalendarItem) _formatRRule(item),
259331
'STATUS:${_formatEventStatus(item.status)}',
260332
'END:VEVENT',
261333
];
262334

335+
String _formatRRule(RepeatingCalendarItem item) {
336+
final parts = <String>['FREQ=${_formatRepeatType(item.repeatType)}'];
337+
if (item.interval > 1) {
338+
parts.add('INTERVAL=${item.interval}');
339+
}
340+
if (item.count > 0) {
341+
parts.add('COUNT=${item.count}');
342+
}
343+
if (item.until != null) {
344+
parts.add('UNTIL=${_formatDateTime(item.until!.toUtc())}');
345+
}
346+
if (item.repeatType == RepeatType.weekly) {
347+
final weekdays = item.weeklyVariationWeekdays;
348+
if (weekdays.isNotEmpty) {
349+
parts.add('BYDAY=${weekdays.map(_weekdayToIcs).join(',')}');
350+
}
351+
} else if (item.repeatType == RepeatType.monthly) {
352+
final monthDays = item.monthlyVariationMonthDays;
353+
if (monthDays.isNotEmpty) {
354+
parts.add('BYMONTHDAY=${monthDays.join(',')}');
355+
}
356+
}
357+
return 'RRULE:${parts.join(';')}';
358+
}
359+
360+
String _weekdayToIcs(int weekday) {
361+
return switch (weekday) {
362+
DateTime.monday => 'MO',
363+
DateTime.tuesday => 'TU',
364+
DateTime.wednesday => 'WE',
365+
DateTime.thursday => 'TH',
366+
DateTime.friday => 'FR',
367+
DateTime.saturday => 'SA',
368+
DateTime.sunday => 'SU',
369+
_ => 'MO',
370+
};
371+
}
372+
263373
String _formatDateTime(DateTime dateTime) =>
264374
"${dateTime.year}${dateTime.month.toString().padLeft(2, '0')}${dateTime.day.toString().padLeft(2, '0')}T${dateTime.hour.toString().padLeft(2, '0')}${dateTime.minute.toString().padLeft(2, '0')}00Z";
265375

api/lib/models/cached.mapper.dart

Lines changed: 12 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)