Skip to content

Commit 93e35ac

Browse files
committed
Improve ical parser and dashboard laoyut
1 parent 4d5abc5 commit 93e35ac

File tree

11 files changed

+359
-165
lines changed

11 files changed

+359
-165
lines changed

api/lib/converters/ical.dart

Lines changed: 158 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,110 +10,221 @@ class ICalConverter {
1010
ICalConverter([this.data]);
1111

1212
void read(List<String> lines, {Event? event, Notebook? notebook}) {
13-
final offset = lines.indexWhere(
14-
(element) => element.trim() == 'BEGIN:VCALENDAR',
15-
);
16-
if (offset == -1) {
17-
return;
13+
final unfoldedLines = <String>[];
14+
for (final line in lines) {
15+
if (line.isEmpty) continue;
16+
if (line.startsWith(' ') || line.startsWith('\t')) {
17+
if (unfoldedLines.isNotEmpty) {
18+
unfoldedLines.last += line.substring(1);
19+
}
20+
} else {
21+
unfoldedLines.add(line.trim());
22+
}
1823
}
24+
1925
CalendarItem? currentItem;
2026
Note? currentNote;
2127
final items = List<CalendarItem>.from(data?.items ?? []);
2228
var currentEvent = event ?? Event(id: createUniqueUint8List());
2329
var currentNotebook = notebook ?? Notebook(id: createUniqueUint8List());
2430
final notes = List<Note>.from(data?.notes ?? []);
25-
for (int i = offset; i < lines.length; i++) {
26-
final line = lines[i];
31+
32+
for (final line in unfoldedLines) {
2733
final parts = line.split(':');
28-
final name = parts[0].trim().split(';');
29-
final key = name.first;
30-
var value = parts.sublist(1).join(':').trim();
31-
value = value
32-
.replaceAll(r'\,', ',')
33-
.replaceAll(r'\;', ';')
34-
.replaceAll(r'\n', '\n');
35-
if (currentItem != null) {
34+
if (parts.length < 2) continue;
35+
36+
final keyPart = parts[0];
37+
var value = parts.sublist(1).join(':');
38+
39+
final keyParts = keyPart.split(';');
40+
final key = keyParts[0].toUpperCase().trim();
41+
42+
value = _unescape(value);
43+
44+
if (key == 'BEGIN') {
45+
if (value == 'VEVENT') {
46+
currentItem = FixedCalendarItem(eventId: currentEvent.id);
47+
} else if (value == 'VTODO') {
48+
currentNote = Note(notebookId: currentNotebook.id!);
49+
}
50+
} else if (key == 'END') {
51+
if (value == 'VEVENT' && currentItem != null) {
52+
items.add(currentItem);
53+
currentItem = null;
54+
} else if (value == 'VTODO' && currentNote != null) {
55+
notes.add(currentNote);
56+
currentNote = null;
57+
}
58+
} else if (currentItem != null) {
3659
switch (key) {
3760
case 'SUMMARY':
3861
currentItem = currentItem.copyWith(name: value);
3962
break;
4063
case 'DESCRIPTION':
4164
currentItem = currentItem.copyWith(description: value);
4265
break;
66+
case 'LOCATION':
67+
currentItem = currentItem.copyWith(location: value);
68+
break;
4369
case 'DTSTART':
44-
currentItem = currentItem.copyWith(start: DateTime.parse(value));
70+
currentItem = currentItem.copyWith(start: _parseDateTime(value));
4571
break;
4672
case 'DTEND':
47-
currentItem = currentItem.copyWith(end: DateTime.parse(value));
73+
currentItem = currentItem.copyWith(end: _parseDateTime(value));
4874
break;
49-
case 'END':
50-
if (value != 'VEVENT') break;
51-
items.add(currentItem);
52-
currentItem = null;
75+
case 'STATUS':
76+
currentItem = currentItem.copyWith(
77+
status: _parseEventStatus(value),
78+
);
5379
break;
5480
}
5581
} else if (currentNote != null) {
5682
switch (key) {
5783
case 'SUMMARY':
5884
currentNote = currentNote.copyWith(name: value);
5985
break;
60-
case 'END':
61-
if (value != 'VTODO') break;
62-
notes.add(currentNote);
63-
currentNote = null;
86+
case 'DESCRIPTION':
87+
currentNote = currentNote.copyWith(description: value);
88+
break;
89+
case 'STATUS':
90+
currentNote = currentNote.copyWith(status: _parseNoteStatus(value));
91+
break;
92+
case 'PRIORITY':
93+
currentNote = currentNote.copyWith(
94+
priority: int.tryParse(value) ?? 0,
95+
);
6496
break;
6597
}
6698
} else {
6799
switch (key) {
68100
case 'NAME':
69101
case 'X-WR-CALNAME':
70102
currentEvent = currentEvent.copyWith(name: value);
103+
currentNotebook = currentNotebook.copyWith(name: value);
71104
break;
72-
case 'BEGIN':
73-
if (value == 'VEVENT') {
74-
currentItem = FixedCalendarItem(eventId: currentEvent.id);
75-
} else if (value == 'VTODO') {
76-
currentNote = Note(notebookId: currentNotebook.id!);
77-
}
78-
continue;
79-
case 'END':
80-
if (value == 'VCALENDAR') {
81-
var current = CachedData(
82-
events: [currentEvent],
83-
items: items,
84-
notes: notes,
85-
);
86-
if (data == null) {
87-
data = current;
88-
} else {
89-
data = data!.concat(current);
90-
}
91-
return;
92-
}
93-
continue;
94105
}
95106
}
96107
}
108+
109+
var current = CachedData(
110+
events: [currentEvent],
111+
items: items,
112+
notes: notes,
113+
notebooks: [currentNotebook],
114+
);
115+
if (data == null) {
116+
data = current;
117+
} else {
118+
data = data!.concat(current);
119+
}
120+
}
121+
122+
String _unescape(String value) {
123+
return value.replaceAllMapped(RegExp(r'\\[,;\\nN]'), (match) {
124+
final s = match.group(0)!;
125+
switch (s.toLowerCase()) {
126+
case r'\,':
127+
return ',';
128+
case r'\;':
129+
return ';';
130+
case r'\\':
131+
return r'\';
132+
case r'\n':
133+
return '\n';
134+
default:
135+
return s;
136+
}
137+
});
138+
}
139+
140+
DateTime? _parseDateTime(String value) {
141+
if (value.length == 8) {
142+
return DateTime.tryParse(
143+
"${value.substring(0, 4)}-${value.substring(4, 6)}-${value.substring(6, 8)}",
144+
);
145+
} else if (value.length >= 15) {
146+
if (value[8] == 'T') {
147+
var iso =
148+
"${value.substring(0, 4)}-${value.substring(4, 6)}-${value.substring(6, 8)}T${value.substring(9, 11)}:${value.substring(11, 13)}:${value.substring(13, 15)}";
149+
if (value.endsWith('Z')) {
150+
iso += 'Z';
151+
}
152+
return DateTime.tryParse(iso);
153+
}
154+
}
155+
return DateTime.tryParse(value);
156+
}
157+
158+
EventStatus _parseEventStatus(String value) {
159+
switch (value.toUpperCase()) {
160+
case 'TENTATIVE':
161+
return EventStatus.draft;
162+
case 'CANCELLED':
163+
return EventStatus.cancelled;
164+
case 'CONFIRMED':
165+
default:
166+
return EventStatus.confirmed;
167+
}
168+
}
169+
170+
NoteStatus _parseNoteStatus(String value) {
171+
switch (value.toUpperCase()) {
172+
case 'COMPLETED':
173+
return NoteStatus.done;
174+
case 'IN-PROCESS':
175+
return NoteStatus.inProgress;
176+
case 'NEEDS-ACTION':
177+
default:
178+
return NoteStatus.todo;
179+
}
97180
}
98181

99182
List<String> writeEvent(CalendarItem item) => [
100183
'BEGIN:VEVENT',
101184
'SUMMARY:${item.name}',
102185
'DESCRIPTION:${item.description}',
186+
if (item.location.isNotEmpty) 'LOCATION:${item.location}',
103187
if (item.start != null) 'DTSTART:${_formatDateTime(item.start!.toUtc())}',
104188
if (item.end != null) 'DTEND:${_formatDateTime(item.end!.toUtc())}',
189+
'STATUS:${_formatEventStatus(item.status)}',
105190
'END:VEVENT',
106191
];
107192

108193
String _formatDateTime(DateTime dateTime) =>
109194
"${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";
110195

196+
String _formatEventStatus(EventStatus status) {
197+
switch (status) {
198+
case EventStatus.draft:
199+
return 'TENTATIVE';
200+
case EventStatus.cancelled:
201+
return 'CANCELLED';
202+
case EventStatus.confirmed:
203+
return 'CONFIRMED';
204+
}
205+
}
206+
111207
List<String> writeNote(Note note) => [
112208
'BEGIN:VTODO',
113209
'SUMMARY:${note.name}',
210+
'DESCRIPTION:${note.description}',
211+
'STATUS:${_formatNoteStatus(note.status)}',
212+
if (note.priority != 0) 'PRIORITY:${note.priority}',
114213
'END:VTODO',
115214
];
116215

216+
String _formatNoteStatus(NoteStatus? status) {
217+
switch (status) {
218+
case NoteStatus.done:
219+
return 'COMPLETED';
220+
case NoteStatus.inProgress:
221+
return 'IN-PROCESS';
222+
case NoteStatus.todo:
223+
default:
224+
return 'NEEDS-ACTION';
225+
}
226+
}
227+
117228
List<String> write([Event? event]) {
118229
final lines = <String>[];
119230
lines.add('BEGIN:VCALENDAR');

app/android/Gemfile.lock

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ GEM
88
artifactory (3.0.17)
99
atomos (0.1.3)
1010
aws-eventstream (1.4.0)
11-
aws-partitions (1.1198.0)
11+
aws-partitions (1.1200.0)
1212
aws-sdk-core (3.240.0)
1313
aws-eventstream (~> 1, >= 1.3.0)
1414
aws-partitions (~> 1, >= 1.992.0)
@@ -20,14 +20,14 @@ GEM
2020
aws-sdk-kms (1.118.0)
2121
aws-sdk-core (~> 3, >= 3.239.1)
2222
aws-sigv4 (~> 1.5)
23-
aws-sdk-s3 (1.208.0)
23+
aws-sdk-s3 (1.209.0)
2424
aws-sdk-core (~> 3, >= 3.234.0)
2525
aws-sdk-kms (~> 1)
2626
aws-sigv4 (~> 1.5)
2727
aws-sigv4 (1.12.1)
2828
aws-eventstream (~> 1, >= 1.0.2)
2929
babosa (1.0.4)
30-
base64 (0.3.0)
30+
base64 (0.2.0)
3131
bigdecimal (4.0.1)
3232
claide (1.1.0)
3333
colored (1.2)
@@ -61,7 +61,7 @@ GEM
6161
faraday-em_synchrony (1.0.1)
6262
faraday-excon (1.1.0)
6363
faraday-httpclient (1.0.1)
64-
faraday-multipart (1.1.1)
64+
faraday-multipart (1.2.0)
6565
multipart-post (~> 2.0)
6666
faraday-net_http (1.0.2)
6767
faraday-net_http_persistent (1.2.0)
@@ -71,13 +71,14 @@ GEM
7171
faraday_middleware (1.2.1)
7272
faraday (~> 1.0)
7373
fastimage (2.4.0)
74-
fastlane (2.229.0)
74+
fastlane (2.230.0)
7575
CFPropertyList (>= 2.3, < 4.0.0)
7676
abbrev (~> 0.1.2)
7777
addressable (>= 2.8, < 3.0.0)
7878
artifactory (~> 3.0)
7979
aws-sdk-s3 (~> 1.0)
8080
babosa (>= 1.0.3, < 2.0.0)
81+
base64 (~> 0.2.0)
8182
bundler (>= 1.12.0, < 3.0.0)
8283
colored (~> 1.2)
8384
commander (~> 4.6)
@@ -99,10 +100,12 @@ GEM
99100
http-cookie (~> 1.0.5)
100101
json (< 3.0.0)
101102
jwt (>= 2.1.0, < 3)
103+
logger (>= 1.6, < 2.0)
102104
mini_magick (>= 4.9.4, < 5.0.0)
103105
multipart-post (>= 2.0.0, < 3.0.0)
104106
mutex_m (~> 0.3.0)
105107
naturally (~> 2.2)
108+
nkf (~> 0.2.0)
106109
optparse (>= 0.1.1, < 1.0.0)
107110
plist (>= 3.1.0, < 4.0.0)
108111
rubyzip (>= 2.0.0, < 3.0.0)
@@ -167,15 +170,16 @@ GEM
167170
logger (1.7.0)
168171
mini_magick (4.13.2)
169172
mini_mime (1.1.5)
170-
multi_json (1.18.0)
173+
multi_json (1.19.1)
171174
multipart-post (2.4.1)
172175
mutex_m (0.3.0)
173176
nanaimo (0.4.0)
174177
naturally (2.3.0)
178+
nkf (0.2.0)
175179
optparse (0.8.1)
176180
os (1.1.4)
177181
plist (3.7.2)
178-
public_suffix (7.0.0)
182+
public_suffix (7.0.1)
179183
rake (13.3.1)
180184
representable (3.2.0)
181185
declarative (< 0.1.0)
@@ -222,8 +226,7 @@ GEM
222226
xcpretty (~> 0.2, >= 0.0.7)
223227

224228
PLATFORMS
225-
x64-mingw-ucrt
226-
x64-unknown
229+
ruby
227230
x86_64-linux
228231

229232
DEPENDENCIES
@@ -232,4 +235,4 @@ DEPENDENCIES
232235
screengrab
233236

234237
BUNDLED WITH
235-
2.7.2
238+
2.7.2

0 commit comments

Comments
 (0)