@@ -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' );
0 commit comments