Skip to content

Commit 2bb9adb

Browse files
example app
1 parent 1cac5af commit 2bb9adb

File tree

12 files changed

+508
-297
lines changed

12 files changed

+508
-297
lines changed

android/src/main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.ContentResolver
44
import android.content.ContentValues
55
import android.net.Uri
66
import android.provider.CalendarContract
7+
import androidx.core.database.getLongOrNull
78
import androidx.core.database.getStringOrNull
89
import kotlinx.coroutines.CompletableDeferred
910
import kotlinx.coroutines.CoroutineScope
@@ -14,7 +15,9 @@ import kotlinx.coroutines.withContext
1415
import java.time.Instant
1516
import java.time.LocalDateTime
1617
import java.time.ZoneOffset
18+
import java.time.ZonedDateTime
1719
import java.util.concurrent.CountDownLatch
20+
import java.util.concurrent.TimeUnit
1821
import kotlin.coroutines.resume
1922
import kotlin.coroutines.resumeWithException
2023
import kotlin.time.Duration
@@ -304,21 +307,30 @@ class CalendarImplem(
304307
put(CalendarContract.Events.EVENT_TIMEZONE, "UTC")
305308
put(CalendarContract.Events.ALL_DAY, isAllDay)
306309

307-
// FIXME: temporary
308-
put(CalendarContract.Events.DTEND, endDate)
309-
310310
if (rRule != null) {
311311
// https://developer.android.com/reference/android/provider/CalendarContract.Events#operations
312-
//val duration = endDate - startDate
313-
//put(CalendarContract.Events.DURATION, duration)
312+
val durationInSeconds = (endDate - startDate) / 1000
313+
val days = durationInSeconds / (24 * 3600)
314+
val hours = (durationInSeconds % (24 * 3600)) / 3600
315+
val minutes = (durationInSeconds % 3600) / 60
316+
val seconds = durationInSeconds % 60
317+
318+
val rfc2445Duration = "P" +
319+
(if (days > 0) "${days}D" else "") +
320+
"T" +
321+
(if (hours > 0) "${hours}H" else "") +
322+
(if (minutes > 0) "${minutes}M" else "") +
323+
(if (seconds > 0) "${seconds}S" else "")
324+
325+
put(CalendarContract.Events.DURATION, rfc2445Duration)
314326

315327
// https://stackoverflow.com/a/49515728/24891894
316328
if (!rRule.contains("COUNT=") && !rRule.contains("UNTIL=")) {
317329
rRule.plus(";COUNT=1000")
318330
}
319331
put(CalendarContract.Events.RRULE, rRule.replace("RRULE:", ""))
320332
} else {
321-
//put(CalendarContract.Events.DTEND, endDate)
333+
put(CalendarContract.Events.DTEND, endDate)
322334
}
323335
}
324336

@@ -413,12 +425,14 @@ class CalendarImplem(
413425
CalendarContract.Events.DESCRIPTION,
414426
CalendarContract.Events.DTSTART,
415427
CalendarContract.Events.DTEND,
428+
CalendarContract.Events.DURATION,
416429
CalendarContract.Events.EVENT_TIMEZONE,
417430
CalendarContract.Events.ALL_DAY,
418431
CalendarContract.Events.RRULE,
419432
)
420433
val selection =
421-
CalendarContract.Events.CALENDAR_ID + " = ? AND " + CalendarContract.Events.DTSTART + " >= ? AND " + CalendarContract.Events.DTEND + " <= ?"
434+
CalendarContract.Events.CALENDAR_ID + " = ? AND " + CalendarContract.Events.DTSTART + " >= ?" + " AND " +
435+
CalendarContract.Events.DTSTART + " <= ?"
422436
val selectionArgs = arrayOf(calendarId, startDate.toString(), endDate.toString())
423437

424438
val cursor = contentResolver.query(eventContentUri, projection, selection, selectionArgs, null)
@@ -431,6 +445,7 @@ class CalendarImplem(
431445
val description =
432446
c.getString(c.getColumnIndexOrThrow(CalendarContract.Events.DESCRIPTION))
433447
val start = c.getLong(c.getColumnIndexOrThrow(CalendarContract.Events.DTSTART))
448+
val duration = c.getString(c.getColumnIndexOrThrow(CalendarContract.Events.DURATION))
434449
val end = c.getLong(c.getColumnIndexOrThrow(CalendarContract.Events.DTEND))
435450
val isAllDay = c.getInt(c.getColumnIndexOrThrow(CalendarContract.Events.ALL_DAY)) == 1
436451
val rRule = c.getStringOrNull(c.getColumnIndexOrThrow(CalendarContract.Events.RRULE))
@@ -462,18 +477,24 @@ class CalendarImplem(
462477
attendeesLatch.await()
463478
remindersLatch.await()
464479

480+
val dtEnd = if (end == 0L) {
481+
start + rfc2445DurationToMillis(duration)
482+
} else {
483+
end
484+
}
485+
465486
events.add(
466487
Event(
467488
id = id,
468489
calendarId = calendarId,
469490
title = title,
470491
startDate = start,
471-
endDate = end,
492+
endDate = dtEnd,
472493
reminders = reminders,
473494
attendees = attendees,
474495
description = description,
475496
isAllDay = isAllDay,
476-
rRule = rRule
497+
rRule = "RRULE:$rRule"
477498
)
478499
)
479500
}
@@ -507,6 +528,22 @@ class CalendarImplem(
507528
}
508529
}
509530

531+
private fun rfc2445DurationToMillis(rfc2445Duration: String): Long {
532+
val regex = Regex("P(?:(\\d+)D)?T(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)S)?")
533+
val matchResult = regex.matchEntire(rfc2445Duration)
534+
?: throw IllegalArgumentException("Invalid RFC2445 duration format")
535+
536+
val days = matchResult.groups[1]?.value?.toLong() ?: 0
537+
val hours = matchResult.groups[2]?.value?.toLong() ?: 0
538+
val minutes = matchResult.groups[3]?.value?.toLong() ?: 0
539+
val seconds = matchResult.groups[4]?.value?.toLong() ?: 0
540+
541+
return TimeUnit.DAYS.toMillis(days) +
542+
TimeUnit.HOURS.toMillis(hours) +
543+
TimeUnit.MINUTES.toMillis(minutes) +
544+
TimeUnit.SECONDS.toMillis(seconds)
545+
}
546+
510547
override fun deleteEvent(eventId: String, callback: (Result<Unit>) -> Unit) {
511548
permissionHandler.requestWritePermission { granted ->
512549
if (granted) {

example/lib/calendar/ui/calendar_screen.dart

Lines changed: 131 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -7,137 +7,149 @@ import 'package:eventide_example/calendar/ui/calendar_form.dart';
77
import 'package:eventide_example/event_list/logic/event_list_cubit.dart';
88
import 'package:value_state/value_state.dart';
99

10-
class CalendarScreen extends StatelessWidget {
10+
class CalendarScreen extends StatefulWidget {
1111
const CalendarScreen({super.key});
1212

13+
@override
14+
State<CalendarScreen> createState() => _CalendarScreenState();
15+
}
16+
17+
class _CalendarScreenState extends State<CalendarScreen> {
18+
bool onlyWritableCalendars = true;
19+
1320
@override
1421
Widget build(BuildContext context) {
15-
return SafeArea(
16-
child: BlocBuilder<CalendarCubit, Value<List<ETCalendar>>>(builder: (context, state) {
17-
return Stack(
18-
children: [
19-
CustomScrollView(slivers: [
20-
SliverAppBar(
21-
pinned: true,
22-
title: const Text('Calendar plugin example app'),
23-
actions: [
24-
IconButton(
25-
icon: const Icon(Icons.add),
26-
onPressed: () {
27-
showDialog(
28-
context: context,
29-
builder: (context) => AlertDialog(
30-
title: const Text('Create calendar'),
31-
content: Padding(
32-
padding: const EdgeInsets.symmetric(horizontal: 16.0),
33-
child: CalendarForm(
34-
onSubmit: (title, color) async {
35-
await BlocProvider.of<CalendarCubit>(context)
36-
.createCalendar(title: title, color: color);
37-
},
38-
),
22+
return Scaffold(
23+
body: SafeArea(
24+
child: BlocBuilder<CalendarCubit, Value<List<ETCalendar>>>(
25+
builder: (context, state) => Stack(
26+
children: [
27+
CustomScrollView(
28+
slivers: [
29+
SliverAppBar(
30+
pinned: true,
31+
title: const Text('Eventide'),
32+
actions: [
33+
Row(
34+
children: [
35+
const Text('writable only'),
36+
const SizedBox(width: 8),
37+
Switch(
38+
value: onlyWritableCalendars,
39+
onChanged: (value) {
40+
setState(() {
41+
onlyWritableCalendars = value;
42+
});
43+
BlocProvider.of<CalendarCubit>(context).fetchCalendars(onlyWritable: value);
44+
},
3945
),
40-
),
41-
);
42-
},
46+
],
47+
),
48+
IconButton(
49+
icon: const Icon(Icons.refresh),
50+
onPressed: () =>
51+
BlocProvider.of<CalendarCubit>(context).fetchCalendars(onlyWritable: onlyWritableCalendars),
52+
),
53+
],
4354
),
44-
],
45-
),
46-
if (state case Value(:final data?))
47-
SliverList(
48-
delegate: SliverChildListDelegate(
49-
data
50-
.map((calendar) => SizedBox(
51-
height: 50,
52-
child: Padding(
53-
padding: const EdgeInsets.symmetric(horizontal: 16),
54-
child: InkWell(
55-
onTap: () async {
56-
try {
57-
await BlocProvider.of<EventListCubit>(context).selectCalendar(calendar);
58-
if (context.mounted) {
59-
Navigator.of(context).push(
60-
MaterialPageRoute(builder: (context) => const EventList()),
61-
);
62-
}
63-
} catch (error) {
64-
if (context.mounted) {
65-
ScaffoldMessenger.of(context)
66-
.showSnackBar(SnackBar(content: Text('Error: ${error.toString()}')));
67-
}
68-
}
69-
},
70-
child: Row(
71-
children: [
72-
Container(
73-
color: calendar.color,
74-
width: 16,
75-
height: 16,
55+
if (state case Value(:final data?))
56+
SliverList(
57+
delegate: SliverChildListDelegate(
58+
data
59+
.map((calendar) => SizedBox(
60+
height: 50,
61+
child: Padding(
62+
padding: const EdgeInsets.symmetric(horizontal: 16),
63+
child: InkWell(
64+
onTap: () async {
65+
try {
66+
await BlocProvider.of<EventListCubit>(context).selectCalendar(calendar);
67+
if (context.mounted) {
68+
Navigator.of(context).push(
69+
MaterialPageRoute(builder: (context) => const EventList()),
70+
);
71+
}
72+
} catch (error) {
73+
if (context.mounted) {
74+
ScaffoldMessenger.of(context)
75+
.showSnackBar(SnackBar(content: Text('Error: ${error.toString()}')));
76+
}
77+
}
78+
},
79+
child: Row(
80+
children: [
81+
Container(
82+
color: calendar.color,
83+
width: 16,
84+
height: 16,
85+
),
86+
const SizedBox(width: 16),
87+
Expanded(
88+
child: Text(
89+
calendar.title,
90+
maxLines: 3,
91+
overflow: TextOverflow.fade,
92+
),
93+
),
94+
const SizedBox(width: 16),
95+
if (calendar.isWritable)
96+
IconButton(
97+
icon: const Icon(Icons.delete),
98+
onPressed: () {
99+
BlocProvider.of<CalendarCubit>(context).deleteCalendar(calendar.id);
100+
},
101+
),
102+
const SizedBox(width: 16),
103+
const Icon(Icons.arrow_right),
104+
],
76105
),
77-
const SizedBox(width: 16),
78-
Expanded(
79-
child: Text(
80-
calendar.title,
81-
maxLines: 3,
82-
overflow: TextOverflow.fade,
83-
),
84-
),
85-
const SizedBox(width: 16),
86-
if (calendar.isWritable)
87-
IconButton(
88-
icon: const Icon(Icons.delete),
89-
onPressed: () {
90-
BlocProvider.of<CalendarCubit>(context).deleteCalendar(calendar.id);
91-
},
92-
),
93-
const SizedBox(width: 16),
94-
const Icon(Icons.arrow_right),
95-
],
106+
),
96107
),
97-
),
98-
),
99-
))
100-
.toList(),
101-
),
102-
),
103-
SliverToBoxAdapter(
104-
child: Column(
105-
crossAxisAlignment: CrossAxisAlignment.center,
106-
mainAxisAlignment: MainAxisAlignment.center,
107-
children: [
108-
if (state case Value(:final data?) when data.isEmpty) ...[
109-
const Text('No calendars found'),
110-
const SizedBox(height: 16),
111-
],
112-
if (state case Value(:final error?)) ...[
113-
Text('Error: ${error.toString()}'),
114-
const SizedBox(height: 16),
115-
],
116-
],
117-
),
118-
),
119-
]),
120-
Positioned(
121-
right: 16,
122-
bottom: 16,
123-
child: Column(
124-
crossAxisAlignment: CrossAxisAlignment.end,
125-
children: [
126-
ElevatedButton(
127-
onPressed: () => BlocProvider.of<CalendarCubit>(context).fetchCalendars(onlyWritable: true),
128-
child: const Text('Writable calendars'),
129-
),
130-
const SizedBox(height: 8),
131-
ElevatedButton(
132-
onPressed: () => BlocProvider.of<CalendarCubit>(context).fetchCalendars(onlyWritable: false),
133-
child: const Text('All calendars'),
108+
))
109+
.toList(),
110+
),
111+
),
112+
SliverToBoxAdapter(
113+
child: Column(
114+
crossAxisAlignment: CrossAxisAlignment.center,
115+
mainAxisAlignment: MainAxisAlignment.center,
116+
children: [
117+
if (state case Value(:final data?) when data.isEmpty) ...[
118+
const Text('No calendars found'),
119+
const SizedBox(height: 16),
120+
],
121+
if (state case Value(:final error?)) ...[
122+
Text('Error: ${error.toString()}'),
123+
const SizedBox(height: 16),
124+
],
125+
],
126+
),
134127
),
135128
],
136129
),
130+
],
131+
),
132+
),
133+
),
134+
floatingActionButton: FloatingActionButton(
135+
child: const Icon(Icons.add),
136+
onPressed: () {
137+
showDialog(
138+
context: context,
139+
builder: (context) => AlertDialog(
140+
title: const Text('Create calendar'),
141+
content: Padding(
142+
padding: const EdgeInsets.symmetric(horizontal: 16.0),
143+
child: CalendarForm(
144+
onSubmit: (title, color) async {
145+
await BlocProvider.of<CalendarCubit>(context).createCalendar(title: title, color: color);
146+
},
147+
),
148+
),
137149
),
138-
],
139-
);
140-
}),
150+
);
151+
},
152+
),
141153
);
142154
}
143155
}

0 commit comments

Comments
 (0)