@@ -4,6 +4,14 @@ import 'package:flow_api/models/event/model.dart';
44import 'package:flow_api/models/note/model.dart' ;
55import '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+
715class 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) ?? []);
0 commit comments