Skip to content

Commit 3f2aacf

Browse files
committed
Improve CalDAV synchronization and update iCal parser with RRULE support
1 parent cb37d27 commit 3f2aacf

6 files changed

Lines changed: 255 additions & 43 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ jobs:
327327
./appimagetool.AppImage AppDir linwood-flow-linux-${{ matrix.arch.name }}.AppImage
328328
- name: Copy nessessary files
329329
run: |
330-
cp assets/images/logo.svg build/linux/${{ matrix.arch.dir }}/release/bundle/dev.linwood.flow.svg
330+
cp images/logo.svg build/linux/${{ matrix.arch.dir }}/release/bundle/dev.linwood.flow.svg
331331
mkdir -p build/linux/${{ matrix.arch.dir }}/release/bundle/usr/share
332332
cp -r linux/debian/usr/share build/linux/${{ matrix.arch.dir }}/release/bundle/usr
333333
- name: Copy portable start script

api/lib/converters/ical.dart

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import 'package:flow_api/models/event/model.dart';
44
import 'package:flow_api/models/note/model.dart';
55
import 'package:flow_api/services/database.dart';
66

7+
class _RRule {
8+
final RepeatType repeatType;
9+
final int interval;
10+
final int count;
11+
final DateTime? until;
12+
_RRule(this.repeatType, this.interval, this.count, this.until);
13+
}
14+
715
class ICalConverter {
816
CachedData? data;
917

@@ -77,6 +85,27 @@ class ICalConverter {
7785
status: _parseEventStatus(value),
7886
);
7987
break;
88+
case 'RRULE':
89+
final rrule = _parseRRule(value);
90+
if (rrule != null && currentItem is FixedCalendarItem) {
91+
final c = currentItem;
92+
currentItem = RepeatingCalendarItem(
93+
id: c.id,
94+
name: c.name,
95+
description: c.description,
96+
location: c.location,
97+
eventId: c.eventId,
98+
start: c.start,
99+
end: c.end,
100+
status: c.status,
101+
repeatType: rrule.repeatType,
102+
interval: rrule.interval,
103+
variation: 0,
104+
count: rrule.count,
105+
until: rrule.until,
106+
);
107+
}
108+
break;
80109
}
81110
} else if (currentNote != null) {
82111
switch (key) {
@@ -137,6 +166,14 @@ class ICalConverter {
137166
});
138167
}
139168

169+
String _escape(String value) {
170+
return value
171+
.replaceAll(r'\', r'\\')
172+
.replaceAll(';', r'\;')
173+
.replaceAll(',', r'\,')
174+
.replaceAll('\n', r'\n');
175+
}
176+
140177
DateTime? _parseDateTime(String value) {
141178
if (value.length == 8) {
142179
return DateTime.tryParse(
@@ -155,6 +192,37 @@ class ICalConverter {
155192
return DateTime.tryParse(value);
156193
}
157194

195+
_RRule? _parseRRule(String value) {
196+
var type = RepeatType.daily;
197+
int interval = 1;
198+
int count = 0;
199+
DateTime? until;
200+
201+
final parts = value.split(';');
202+
for (final part in parts) {
203+
final kv = part.split('=');
204+
if (kv.length != 2) continue;
205+
final k = kv[0].toUpperCase();
206+
final v = kv[1];
207+
if (k == 'FREQ') {
208+
type = switch (v.toUpperCase()) {
209+
'DAILY' => RepeatType.daily,
210+
'WEEKLY' => RepeatType.weekly,
211+
'MONTHLY' => RepeatType.monthly,
212+
'YEARLY' => RepeatType.yearly,
213+
_ => type,
214+
};
215+
} else if (k == 'INTERVAL') {
216+
interval = int.tryParse(v) ?? 1;
217+
} else if (k == 'COUNT') {
218+
count = int.tryParse(v) ?? 0;
219+
} else if (k == 'UNTIL') {
220+
until = _parseDateTime(v);
221+
}
222+
}
223+
return _RRule(type, interval, count, until);
224+
}
225+
158226
EventStatus _parseEventStatus(String value) {
159227
switch (value.toUpperCase()) {
160228
case 'TENTATIVE':
@@ -181,18 +249,33 @@ class ICalConverter {
181249

182250
List<String> writeEvent(CalendarItem item) => [
183251
'BEGIN:VEVENT',
184-
'SUMMARY:${item.name}',
185-
'DESCRIPTION:${item.description}',
186-
if (item.location.isNotEmpty) 'LOCATION:${item.location}',
252+
'SUMMARY:${_escape(item.name)}',
253+
'DESCRIPTION:${_escape(item.description)}',
254+
if (item.location.isNotEmpty) 'LOCATION:${_escape(item.location)}',
187255
if (item.start != null) 'DTSTART:${_formatDateTime(item.start!.toUtc())}',
188256
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())}' : ''}',
189259
'STATUS:${_formatEventStatus(item.status)}',
190260
'END:VEVENT',
191261
];
192262

193263
String _formatDateTime(DateTime dateTime) =>
194264
"${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";
195265

266+
String _formatRepeatType(RepeatType type) {
267+
switch (type) {
268+
case RepeatType.daily:
269+
return 'DAILY';
270+
case RepeatType.weekly:
271+
return 'WEEKLY';
272+
case RepeatType.monthly:
273+
return 'MONTHLY';
274+
case RepeatType.yearly:
275+
return 'YEARLY';
276+
}
277+
}
278+
196279
String _formatEventStatus(EventStatus status) {
197280
switch (status) {
198281
case EventStatus.draft:
@@ -206,8 +289,8 @@ class ICalConverter {
206289

207290
List<String> writeNote(Note note) => [
208291
'BEGIN:VTODO',
209-
'SUMMARY:${note.name}',
210-
'DESCRIPTION:${note.description}',
292+
'SUMMARY:${_escape(note.name)}',
293+
'DESCRIPTION:${_escape(note.description)}',
211294
'STATUS:${_formatNoteStatus(note.status)}',
212295
if (note.priority != 0) 'PRIORITY:${note.priority}',
213296
'END:VTODO',
@@ -230,8 +313,8 @@ class ICalConverter {
230313
lines.add('BEGIN:VCALENDAR');
231314
lines.add('VERSION:2.0');
232315
if (event != null) {
233-
lines.add('NAME:${event.name}');
234-
lines.add('X-WR-CALNAME:${event.name}');
316+
lines.add('NAME:${_escape(event.name)}');
317+
lines.add('X-WR-CALNAME:${_escape(event.name)}');
235318
}
236319
lines.addAll(data?.items.expand(writeEvent) ?? []);
237320
lines.addAll(data?.notes.expand(writeNote) ?? []);

api/lib/models/note/database.dart

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,3 +384,71 @@ class LabelNoteDatabaseConnector extends NoteDatabaseConnector<Label>
384384
@override
385385
Label decode(Map<String, dynamic> data) => Label.fromDatabase(data);
386386
}
387+
388+
abstract class NoteDatabaseServiceLinker extends NoteService with TableService {
389+
final NoteDatabaseService service;
390+
391+
NoteDatabaseServiceLinker(this.service);
392+
393+
@override
394+
FutureOr<List<Note>> getNotes({
395+
int offset = 0,
396+
int limit = 50,
397+
Uint8List? parent,
398+
Uint8List? notebook,
399+
Set<NoteStatus?> statuses = const {
400+
NoteStatus.todo,
401+
NoteStatus.inProgress,
402+
NoteStatus.done,
403+
null,
404+
},
405+
String search = '',
406+
}) =>
407+
service.getNotes(
408+
offset: offset,
409+
limit: limit,
410+
parent: parent,
411+
notebook: notebook,
412+
statuses: statuses,
413+
search: search,
414+
);
415+
416+
@override
417+
FutureOr<Note?> createNote(Note note) => service.createNote(note);
418+
419+
@override
420+
FutureOr<bool> updateNote(Note note) => service.updateNote(note);
421+
422+
@override
423+
FutureOr<bool> deleteNote(Uint8List id) => service.deleteNote(id);
424+
425+
@override
426+
FutureOr<Note?> getNote(Uint8List id, {bool fallback = false}) =>
427+
service.getNote(id, fallback: fallback);
428+
429+
@override
430+
FutureOr<List<Notebook>> getNotebooks({
431+
int offset = 0,
432+
int limit = 50,
433+
String search = '',
434+
}) =>
435+
service.getNotebooks(offset: offset, limit: limit, search: search);
436+
437+
@override
438+
FutureOr<Notebook?> createNotebook(Notebook notebook) =>
439+
service.createNotebook(notebook);
440+
441+
@override
442+
FutureOr<bool> updateNotebook(Notebook notebook) =>
443+
service.updateNotebook(notebook);
444+
445+
@override
446+
FutureOr<bool> deleteNotebook(Uint8List id) => service.deleteNotebook(id);
447+
448+
@override
449+
FutureOr<Notebook?> getNotebook(Uint8List id) => service.getNotebook(id);
450+
451+
@override
452+
FutureOr<void> clear() => service.clear();
453+
}
454+

0 commit comments

Comments
 (0)