From bc9d4754214848b9d5c53c4f78b2c7e3b03592e1 Mon Sep 17 00:00:00 2001 From: thomassth Date: Fri, 30 Sep 2022 16:38:21 -0400 Subject: [PATCH 01/43] fix for ownerAccount null --- .../com/builttoroam/devicecalendar/models/Calendar.kt | 11 +++++++++-- example/android/app/build.gradle | 2 +- example/android/build.gradle | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt index da17b0d3..6e10b7fe 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt @@ -1,6 +1,13 @@ package com.builttoroam.devicecalendar.models -class Calendar(val id: String, val name: String, val color : Int, val accountName: String, val accountType: String, val ownerAccount: String) { +class Calendar( + val id: String, + val name: String, + val color: Int, + val accountName: String, + val accountType: String, + val ownerAccount: String? +) { var isReadOnly: Boolean = false var isDefault: Boolean = false -} \ No newline at end of file +} diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 4af82323..dd924715 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion 32 ndkVersion '22.1.7171670' sourceSets { diff --git a/example/android/build.gradle b/example/android/build.gradle index c85ba2ad..d135914c 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.3' + classpath 'com.android.tools.build:gradle:7.1.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } From 682b43b7073e3e618e932de2bbf4c76a35813b29 Mon Sep 17 00:00:00 2001 From: thomassth Date: Fri, 30 Sep 2022 16:55:45 -0400 Subject: [PATCH 02/43] more calendar data displayed --- example/lib/presentation/pages/calendars.dart | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index e28dbc7f..98601e14 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -70,12 +70,20 @@ class _CalendarsPageState extends State { child: Row( children: [ Expanded( - flex: 1, - child: Text( - _calendars[index].name!, - style: Theme.of(context).textTheme.subtitle1, - ), - ), + flex: 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${_calendars[index].id}: ${_calendars[index].name!}", + style: + Theme.of(context).textTheme.subtitle1, + ), + Text( + "Account: ${_calendars[index].accountName!}"), + Text( + "type: ${_calendars[index].accountType}"), + ])), Container( width: 15, height: 15, @@ -84,13 +92,14 @@ class _CalendarsPageState extends State { color: Color(_calendars[index].color!)), ), SizedBox(width: 10), - Container( - margin: const EdgeInsets.fromLTRB(0, 0, 5.0, 0), - padding: const EdgeInsets.all(3.0), - decoration: BoxDecoration( - border: Border.all(color: Colors.blueAccent)), - child: Text('Default'), - ), + if (_calendars[index].isDefault!) + Container( + margin: const EdgeInsets.fromLTRB(0, 0, 5.0, 0), + padding: const EdgeInsets.all(3.0), + decoration: BoxDecoration( + border: Border.all(color: Colors.blueAccent)), + child: Text('Default'), + ), Icon(_calendars[index].isReadOnly == true ? Icons.lock : Icons.lock_open) From 996283545f9d08dc4006aedef60b3a8f026a1516 Mon Sep 17 00:00:00 2001 From: thomassth Date: Sat, 1 Oct 2022 01:19:38 -0400 Subject: [PATCH 03/43] example app dark mode --- example/lib/main.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index 043898fc..f24e43ba 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -14,6 +14,9 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { return MaterialApp( + theme: ThemeData(), + themeMode: ThemeMode.system, + darkTheme: ThemeData.dark(), routes: { AppRoutes.calendars: (context) { return CalendarsPage(key: Key('calendarsPage')); From 23ff57e2764efd66f77c4a7c471dc2c0619ad2eb Mon Sep 17 00:00:00 2001 From: thomassth Date: Thu, 6 Oct 2022 01:54:24 -0400 Subject: [PATCH 04/43] activate flutter lint --- analysis_options.yaml | 30 + example/analysis_options.yaml | 31 +- example/integration_test/app_test.dart | 4 +- .../integration_test/integration_test.dart | 2 +- example/lib/common/app_routes.dart | 2 +- example/lib/main.dart | 6 +- .../lib/presentation/date_time_picker.dart | 4 +- example/lib/presentation/event_item.dart | 68 +- .../lib/presentation/pages/calendar_add.dart | 12 +- .../presentation/pages/calendar_event.dart | 1274 +++++++++-------- .../presentation/pages/calendar_events.dart | 26 +- example/lib/presentation/pages/calendars.dart | 14 +- .../presentation/pages/event_attendee.dart | 8 +- .../presentation/pages/event_reminders.dart | 10 +- .../presentation/recurring_event_dialog.dart | 12 +- example/pubspec.yaml | 1 + lib/src/common/error_messages.dart | 4 +- lib/src/device_calendar.dart | 15 +- lib/src/models/event.dart | 4 +- test/device_calendar_test.dart | 37 +- 20 files changed, 816 insertions(+), 748 deletions(-) create mode 100644 analysis_options.yaml diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..68a79339 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + constant_identifier_names: false # TODO: use lowerCamelCases consistently + avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options \ No newline at end of file diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index d4fcc1ad..3e1200f9 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1 +1,30 @@ -include: package:pedantic/analysis_options.yaml \ No newline at end of file +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + constant_identifier_names: false # TODO: use lowerCamelCases consistently + avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/example/integration_test/app_test.dart b/example/integration_test/app_test.dart index 503a8951..6e4a1908 100644 --- a/example/integration_test/app_test.dart +++ b/example/integration_test/app_test.dart @@ -10,7 +10,7 @@ import 'package:device_calendar_example/main.dart' as app; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('Calendar plugin example', () { - final eventTitle = Uuid().v1(); + final eventTitle = const Uuid().v1(); final saveEventButtonFinder = find.byKey(const Key('saveEventButton')); final eventTitleFinder = find.text(eventTitle); final firstWritableCalendarFinder = @@ -27,7 +27,7 @@ void main() { testWidgets('select first writable calendar', (WidgetTester tester) async { app.main(); - await tester.pumpAndSettle(Duration(milliseconds: 500)); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(firstWritableCalendarFinder, findsOneWidget); }); testWidgets('go to add event page', (WidgetTester tester) async { diff --git a/example/integration_test/integration_test.dart b/example/integration_test/integration_test.dart index d061d76e..ca6e9ef3 100644 --- a/example/integration_test/integration_test.dart +++ b/example/integration_test/integration_test.dart @@ -2,7 +2,7 @@ import 'package:integration_test/integration_test_driver.dart'; /// Instruction for iOS: /// See `ios.sh` -/// Instruction for android: +/// Instruction for android: /// See `integration_test_android.dart` Future main() => integrationDriver(); diff --git a/example/lib/common/app_routes.dart b/example/lib/common/app_routes.dart index 176a028b..991a9d70 100644 --- a/example/lib/common/app_routes.dart +++ b/example/lib/common/app_routes.dart @@ -1,3 +1,3 @@ class AppRoutes { - static final calendars = '/'; + static const calendars = '/'; } diff --git a/example/lib/main.dart b/example/lib/main.dart index f24e43ba..ffb03586 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,9 +3,11 @@ import 'package:flutter/material.dart'; import 'common/app_routes.dart'; import 'presentation/pages/calendars.dart'; -void main() => runApp(MyApp()); +void main() => runApp(const MyApp()); class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + @override _MyAppState createState() => _MyAppState(); } @@ -19,7 +21,7 @@ class _MyAppState extends State { darkTheme: ThemeData.dark(), routes: { AppRoutes.calendars: (context) { - return CalendarsPage(key: Key('calendarsPage')); + return const CalendarsPage(key: Key('calendarsPage')); } }, ); diff --git a/example/lib/presentation/date_time_picker.dart b/example/lib/presentation/date_time_picker.dart index cf1692e7..dc11e8d9 100644 --- a/example/lib/presentation/date_time_picker.dart +++ b/example/lib/presentation/date_time_picker.dart @@ -23,7 +23,7 @@ class DateTimePicker extends StatelessWidget { final ValueChanged? selectTime; final bool enableTime; - Future _selectDate(BuildContext context) async { + Future _selectDate(BuildContext context) async { final picked = await showDatePicker( context: context, initialDate: selectedDate != null @@ -36,7 +36,7 @@ class DateTimePicker extends StatelessWidget { } } - Future _selectTime(BuildContext context) async { + Future _selectTime(BuildContext context) async { if (selectedTime == null) return; final picked = await showTimePicker(context: context, initialTime: selectedTime!); diff --git a/example/lib/presentation/event_item.dart b/example/lib/presentation/event_item.dart index 2d9c182d..ec0eb425 100644 --- a/example/lib/presentation/event_item.dart +++ b/example/lib/presentation/event_item.dart @@ -16,7 +16,7 @@ class EventItem extends StatefulWidget { final VoidCallback _onLoadingStarted; final Function(bool) _onDeleteFinished; - EventItem( + const EventItem( this._calendarEvent, this._deviceCalendarPlugin, this._onLoadingStarted, @@ -54,15 +54,15 @@ class _EventItemState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0), + const Padding( + padding: EdgeInsets.symmetric(vertical: 10.0), child: FlutterLogo(), ), ListTile( title: Text(widget._calendarEvent?.title ?? ''), subtitle: Text(widget._calendarEvent?.description ?? '')), Container( - padding: EdgeInsets.symmetric(horizontal: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ if (_currentLocation != null) @@ -70,9 +70,9 @@ class _EventItemState extends State { alignment: Alignment.topLeft, child: Row( children: [ - Container( + SizedBox( width: _eventFieldNameWidth, - child: Text('Starts'), + child: const Text('Starts'), ), Text( widget._calendarEvent == null @@ -84,7 +84,7 @@ class _EventItemState extends State { ], ), ), - Padding( + const Padding( padding: EdgeInsets.symmetric(vertical: 5.0), ), if (_currentLocation != null) @@ -92,9 +92,9 @@ class _EventItemState extends State { alignment: Alignment.topLeft, child: Row( children: [ - Container( + SizedBox( width: _eventFieldNameWidth, - child: Text('Ends'), + child: const Text('Ends'), ), Text( widget._calendarEvent?.end == null @@ -106,16 +106,16 @@ class _EventItemState extends State { ], ), ), - SizedBox( + const SizedBox( height: 10.0, ), Align( alignment: Alignment.topLeft, child: Row( children: [ - Container( + SizedBox( width: _eventFieldNameWidth, - child: Text('All day?'), + child: const Text('All day?'), ), Text(widget._calendarEvent?.allDay != null && widget._calendarEvent?.allDay == true @@ -124,16 +124,16 @@ class _EventItemState extends State { ], ), ), - SizedBox( + const SizedBox( height: 10.0, ), Align( alignment: Alignment.topLeft, child: Row( children: [ - Container( + SizedBox( width: _eventFieldNameWidth, - child: Text('Location'), + child: const Text('Location'), ), Expanded( child: Text( @@ -144,16 +144,16 @@ class _EventItemState extends State { ], ), ), - SizedBox( + const SizedBox( height: 10.0, ), Align( alignment: Alignment.topLeft, child: Row( children: [ - Container( + SizedBox( width: _eventFieldNameWidth, - child: Text('URL'), + child: const Text('URL'), ), Expanded( child: Text( @@ -164,16 +164,16 @@ class _EventItemState extends State { ], ), ), - SizedBox( + const SizedBox( height: 10.0, ), Align( alignment: Alignment.topLeft, child: Row( children: [ - Container( + SizedBox( width: _eventFieldNameWidth, - child: Text('Attendees'), + child: const Text('Attendees'), ), Expanded( child: Text( @@ -188,16 +188,16 @@ class _EventItemState extends State { ], ), ), - SizedBox( + const SizedBox( height: 10.0, ), Align( alignment: Alignment.topLeft, child: Row( children: [ - Container( + SizedBox( width: _eventFieldNameWidth, - child: Text('Availability'), + child: const Text('Availability'), ), Expanded( child: Text( @@ -209,16 +209,16 @@ class _EventItemState extends State { ], ), ), - SizedBox( + const SizedBox( height: 10.0, ), Align( alignment: Alignment.topLeft, child: Row( children: [ - Container( + SizedBox( width: _eventFieldNameWidth, - child: Text('Status'), + child: const Text('Status'), ), Expanded( child: Text( @@ -242,7 +242,7 @@ class _EventItemState extends State { widget._onTapped(widget._calendarEvent as Event); } }, - icon: Icon(Icons.edit), + icon: const Icon(Icons.edit), ), IconButton( onPressed: () async { @@ -252,14 +252,14 @@ class _EventItemState extends State { builder: (BuildContext context) { if (widget._calendarEvent?.recurrenceRule == null) { return AlertDialog( - title: Text( + title: const Text( 'Are you sure you want to delete this event?'), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, - child: Text('Cancel'), + child: const Text('Cancel'), ), TextButton( onPressed: () async { @@ -274,13 +274,13 @@ class _EventItemState extends State { deleteResult.isSuccess && deleteResult.data != null); }, - child: Text('Delete'), + child: const Text('Delete'), ), ], ); } else { if (widget._calendarEvent == null) { - return SizedBox(); + return const SizedBox(); } return RecurringEventDialog( widget._deviceCalendarPlugin, @@ -291,7 +291,7 @@ class _EventItemState extends State { }, ); }, - icon: Icon(Icons.delete), + icon: const Icon(Icons.delete), ), ] else ...[ IconButton( @@ -300,7 +300,7 @@ class _EventItemState extends State { widget._onTapped(widget._calendarEvent!); } }, - icon: Icon(Icons.remove_red_eye), + icon: const Icon(Icons.remove_red_eye), ), ] ], diff --git a/example/lib/presentation/pages/calendar_add.dart b/example/lib/presentation/pages/calendar_add.dart index abce3788..fc4abbce 100644 --- a/example/lib/presentation/pages/calendar_add.dart +++ b/example/lib/presentation/pages/calendar_add.dart @@ -4,6 +4,8 @@ import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; class CalendarAddPage extends StatefulWidget { + const CalendarAddPage({Key? key}) : super(key: key); + @override _CalendarAddPageState createState() { return _CalendarAddPageState(); @@ -29,13 +31,13 @@ class _CalendarAddPageState extends State { return Scaffold( key: _scaffoldKey, appBar: AppBar( - title: Text('Create Calendar'), + title: const Text('Create Calendar'), ), body: Form( autovalidateMode: _autovalidate, key: _formKey, child: Container( - padding: EdgeInsets.all(10), + padding: const EdgeInsets.all(10), child: Column( children: [ TextFormField( @@ -46,11 +48,11 @@ class _CalendarAddPageState extends State { validator: _validateCalendarName, onSaved: (String? value) => _calendarName = value ?? '', ), - SizedBox(height: 10), + const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Calendar Color'), + const Text('Calendar Color'), DropdownButton( onChanged: (selectedColor) { setState(() => _colorChoice = selectedColor); @@ -101,7 +103,7 @@ class _CalendarAddPageState extends State { } } }, - child: Icon(Icons.check), + child: const Icon(Icons.check), ), ); } diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 8dcc3cd0..587520ec 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -4,14 +4,14 @@ import 'package:collection/collection.dart'; import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:intl/intl.dart'; +import 'package:timezone/timezone.dart'; import '../date_time_picker.dart'; import '../recurring_event_dialog.dart'; import 'event_attendee.dart'; import 'event_reminders.dart'; -import 'package:timezone/timezone.dart'; -import 'package:flutter_native_timezone/flutter_native_timezone.dart'; enum RecurrenceRuleEndType { Indefinite, MaxOccurrences, SpecifiedEndDate } @@ -89,11 +89,13 @@ class _CalendarEventPageState extends State { var currentLocation = timeZoneDatabase.locations[_timezone]; if (currentLocation != null) { _startDate = TZDateTime.now(currentLocation); - _endDate = TZDateTime.now(currentLocation).add(Duration(hours: 1)); + _endDate = + TZDateTime.now(currentLocation).add(const Duration(hours: 1)); } else { var fallbackLocation = timeZoneDatabase.locations['Etc/UTC']; _startDate = TZDateTime.now(fallbackLocation!); - _endDate = TZDateTime.now(fallbackLocation).add(Duration(hours: 1)); + _endDate = + TZDateTime.now(fallbackLocation).add(const Duration(hours: 1)); } _event = Event(_calendar.id, start: _startDate, end: _endDate); @@ -178,300 +180,275 @@ class _CalendarEventPageState extends State { ? 'View event ${_event?.title}' : 'Edit event ${_event?.title}'), ), - body: SingleChildScrollView( - child: AbsorbPointer( - absorbing: _calendar.isReadOnly ?? false, - child: Column( - children: [ - Form( - autovalidateMode: _autovalidate, - key: _formKey, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(10.0), - child: TextFormField( - key: Key('titleField'), - initialValue: _event?.title, - decoration: const InputDecoration( - labelText: 'Title', - hintText: 'Meeting with Gloria...'), - validator: _validateTitle, - onSaved: (String? value) { - _event?.title = value; - }, - ), - ), - Padding( - padding: const EdgeInsets.all(10.0), - child: TextFormField( - initialValue: _event?.description, - decoration: const InputDecoration( - labelText: 'Description', - hintText: 'Remember to buy flowers...'), - onSaved: (String? value) { - _event?.description = value; - }, - ), - ), - Padding( - padding: const EdgeInsets.all(10.0), - child: TextFormField( - initialValue: _event?.location, - decoration: const InputDecoration( - labelText: 'Location', - hintText: 'Sydney, Australia'), - onSaved: (String? value) { - _event?.location = value; - }, + body: SafeArea( + child: SingleChildScrollView( + child: AbsorbPointer( + absorbing: _calendar.isReadOnly ?? false, + child: Column( + children: [ + Form( + autovalidateMode: _autovalidate, + key: _formKey, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + key: const Key('titleField'), + initialValue: _event?.title, + decoration: const InputDecoration( + labelText: 'Title', + hintText: 'Meeting with Gloria...'), + validator: _validateTitle, + onSaved: (String? value) { + _event?.title = value; + }, + ), ), - ), - Padding( - padding: const EdgeInsets.all(10.0), - child: TextFormField( - initialValue: _event?.url?.data?.contentText ?? '', - decoration: const InputDecoration( - labelText: 'URL', hintText: 'https://google.com'), - onSaved: (String? value) { - if (value != null) { - var uri = Uri.dataFromString(value); - _event?.url = uri; - } - }, + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + initialValue: _event?.description, + decoration: const InputDecoration( + labelText: 'Description', + hintText: 'Remember to buy flowers...'), + onSaved: (String? value) { + _event?.description = value; + }, + ), ), - ), - ListTile( - leading: Text( - 'Availability', - style: TextStyle(fontSize: 16), + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + initialValue: _event?.location, + decoration: const InputDecoration( + labelText: 'Location', + hintText: 'Sydney, Australia'), + onSaved: (String? value) { + _event?.location = value; + }, + ), ), - trailing: DropdownButton( - value: _availability, - onChanged: (Availability? newValue) { - setState(() { - if (newValue != null) { - _availability = newValue; - _event?.availability = newValue; + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + initialValue: _event?.url?.data?.contentText ?? '', + decoration: const InputDecoration( + labelText: 'URL', hintText: 'https://google.com'), + onSaved: (String? value) { + if (value != null) { + var uri = Uri.dataFromString(value); + _event?.url = uri; } - }); - }, - items: Availability.values - .map>( - (Availability value) { - return DropdownMenuItem( - value: value, - child: Text(value.enumToString), - ); - }).toList(), + }, + ), ), - ), - if(Platform.isAndroid) ListTile( - leading: Text( - 'Status', + leading: const Text( + 'Availability', style: TextStyle(fontSize: 16), ), - trailing: DropdownButton( - value: _eventStatus, - onChanged: (EventStatus? newValue) { + trailing: DropdownButton( + value: _availability, + onChanged: (Availability? newValue) { setState(() { if (newValue != null) { - _eventStatus = newValue; - _event?.status = newValue; + _availability = newValue; + _event?.availability = newValue; } }); }, - items: EventStatus.values - .map>( - (EventStatus value) { - return DropdownMenuItem( + items: Availability.values + .map>( + (Availability value) { + return DropdownMenuItem( value: value, child: Text(value.enumToString), ); }).toList(), ), ), - SwitchListTile( - value: _event?.allDay ?? false, - onChanged: (value) => - setState(() => _event?.allDay = value), - title: Text('All Day'), - ), - if (_startDate != null) - Padding( - padding: const EdgeInsets.all(10.0), - child: DateTimePicker( - labelText: 'From', - enableTime: _event?.allDay == false, - selectedDate: _startDate, - selectedTime: _startTime, - selectDate: (DateTime date) { - setState(() { - var currentLocation = - timeZoneDatabase.locations[_timezone]; - if (currentLocation != null) { - _startDate = - TZDateTime.from(date, currentLocation); - _event?.start = _combineDateWithTime( - _startDate, _startTime); - } - }); - }, - selectTime: (TimeOfDay time) { - setState( - () { - _startTime = time; - _event?.start = _combineDateWithTime( - _startDate, _startTime); - }, - ); - }, - ), - ), - if ((_event?.allDay == false) && Platform.isAndroid) - Padding( - padding: const EdgeInsets.all(10.0), - child: TextFormField( - initialValue: _event?.start?.location.name, - decoration: const InputDecoration( - labelText: 'Start date time zone', - hintText: 'Australia/Sydney'), - onSaved: (String? value) { - _event?.updateStartLocation(value); - }, + if (Platform.isAndroid) + ListTile( + leading: const Text( + 'Status', + style: TextStyle(fontSize: 16), + ), + trailing: DropdownButton( + value: _eventStatus, + onChanged: (EventStatus? newValue) { + setState(() { + if (newValue != null) { + _eventStatus = newValue; + _event?.status = newValue; + } + }); + }, + items: EventStatus.values + .map>( + (EventStatus value) { + return DropdownMenuItem( + value: value, + child: Text(value.enumToString), + ); + }).toList(), + ), ), + SwitchListTile( + value: _event?.allDay ?? false, + onChanged: (value) => + setState(() => _event?.allDay = value), + title: const Text('All Day'), ), - // Only add the 'To' Date for non-allDay events on all - // platforms except Android (which allows multiple-day allDay events) - if (_event?.allDay == false || Platform.isAndroid) - Padding( - padding: const EdgeInsets.all(10.0), - child: DateTimePicker( - labelText: 'To', - selectedDate: _endDate, - selectedTime: _endTime, - enableTime: _event?.allDay == false, - selectDate: (DateTime date) { - setState( - () { + if (_startDate != null) + Padding( + padding: const EdgeInsets.all(10.0), + child: DateTimePicker( + labelText: 'From', + enableTime: _event?.allDay == false, + selectedDate: _startDate, + selectedTime: _startTime, + selectDate: (DateTime date) { + setState(() { var currentLocation = timeZoneDatabase.locations[_timezone]; if (currentLocation != null) { - _endDate = + _startDate = TZDateTime.from(date, currentLocation); + _event?.start = _combineDateWithTime( + _startDate, _startTime); + } + }); + }, + selectTime: (TimeOfDay time) { + setState( + () { + _startTime = time; + _event?.start = _combineDateWithTime( + _startDate, _startTime); + }, + ); + }, + ), + ), + if ((_event?.allDay == false) && Platform.isAndroid) + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + initialValue: _event?.start?.location.name, + decoration: const InputDecoration( + labelText: 'Start date time zone', + hintText: 'Australia/Sydney'), + onSaved: (String? value) { + _event?.updateStartLocation(value); + }, + ), + ), + // Only add the 'To' Date for non-allDay events on all + // platforms except Android (which allows multiple-day allDay events) + if (_event?.allDay == false || Platform.isAndroid) + Padding( + padding: const EdgeInsets.all(10.0), + child: DateTimePicker( + labelText: 'To', + selectedDate: _endDate, + selectedTime: _endTime, + enableTime: _event?.allDay == false, + selectDate: (DateTime date) { + setState( + () { + var currentLocation = + timeZoneDatabase.locations[_timezone]; + if (currentLocation != null) { + _endDate = + TZDateTime.from(date, currentLocation); + _event?.end = _combineDateWithTime( + _endDate, _endTime); + } + }, + ); + }, + selectTime: (TimeOfDay time) { + setState( + () { + _endTime = time; _event?.end = _combineDateWithTime(_endDate, _endTime); - } - }, - ); - }, - selectTime: (TimeOfDay time) { - setState( - () { - _endTime = time; - _event?.end = - _combineDateWithTime(_endDate, _endTime); - }, - ); - }, + }, + ); + }, + ), ), - ), - if (_event?.allDay == false && Platform.isAndroid) - Padding( - padding: const EdgeInsets.all(10.0), - child: TextFormField( - initialValue: _event?.end?.location.name, - decoration: InputDecoration( - labelText: 'End date time zone', - hintText: 'Australia/Sydney'), - onSaved: (String? value) => - _event?.updateEndLocation(value), + if (_event?.allDay == false && Platform.isAndroid) + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + initialValue: _event?.end?.location.name, + decoration: const InputDecoration( + labelText: 'End date time zone', + hintText: 'Australia/Sydney'), + onSaved: (String? value) => + _event?.updateEndLocation(value), + ), ), - ), - ListTile( - onTap: _calendar.isReadOnly == false - ? () async { - var result = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - EventAttendeePage())); - if (result != null) { - setState(() { - _attendees.add(result); - }); - } - } - : null, - leading: Icon(Icons.people), - title: Text(_calendar.isReadOnly == false - ? 'Add Attendees' - : 'Attendees'), - ), - ListView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: _attendees.length, - itemBuilder: (context, index) { - return Container( - color: _attendees[index].isOrganiser - ? Colors.greenAccent[100] - : Colors.transparent, - child: ListTile( - onTap: () async { - var result = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EventAttendeePage( - attendee: _attendees[index], - eventId: _event?.eventId))); - if (result != null) { - return setState(() { - _attendees[index] = result; - }); + ListTile( + onTap: _calendar.isReadOnly == false + ? () async { + var result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const EventAttendeePage())); + if (result != null) { + setState(() { + _attendees.add(result); + }); + } } - }, - title: Padding( - padding: - const EdgeInsets.symmetric(vertical: 10.0), - child: Text( - '${_attendees[index].name} (${_attendees[index].emailAddress})'), - ), - subtitle: Wrap( - spacing: 10, - direction: Axis.horizontal, - alignment: WrapAlignment.end, - children: [ - Visibility( - visible: _attendees[index] - .androidAttendeeDetails != - null, - child: Container( - margin: const EdgeInsets.symmetric( - vertical: 10.0), - padding: const EdgeInsets.all(3.0), - decoration: BoxDecoration( - border: Border.all( - color: Colors.blueAccent)), - child: Text( - 'Android: ${_attendees[index].androidAttendeeDetails?.attendanceStatus?.enumToString}')), - ), - Visibility( - visible: - _attendees[index].iosAttendeeDetails != - null, - child: Container( - margin: const EdgeInsets.symmetric( - vertical: 10.0), - padding: const EdgeInsets.all(3.0), - decoration: BoxDecoration( - border: Border.all( - color: Colors.blueAccent)), - child: Text( - 'iOS: ${_attendees[index].iosAttendeeDetails?.attendanceStatus?.enumToString}')), - ), - Visibility( - visible: _attendees[index].isCurrentUser, + : null, + leading: const Icon(Icons.people), + title: Text(_calendar.isReadOnly == false + ? 'Add Attendees' + : 'Attendees'), + ), + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: _attendees.length, + itemBuilder: (context, index) { + return Container( + color: _attendees[index].isOrganiser + ? Colors.greenAccent[100] + : Colors.transparent, + child: ListTile( + onTap: () async { + var result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EventAttendeePage( + attendee: _attendees[index], + eventId: _event?.eventId))); + if (result != null) { + return setState(() { + _attendees[index] = result; + }); + } + }, + title: Padding( + padding: + const EdgeInsets.symmetric(vertical: 10.0), + child: Text( + '${_attendees[index].name} (${_attendees[index].emailAddress})'), + ), + subtitle: Wrap( + spacing: 10, + direction: Axis.horizontal, + alignment: WrapAlignment.end, + children: [ + Visibility( + visible: _attendees[index] + .androidAttendeeDetails != + null, child: Container( margin: const EdgeInsets.symmetric( vertical: 10.0), @@ -479,9 +456,13 @@ class _CalendarEventPageState extends State { decoration: BoxDecoration( border: Border.all( color: Colors.blueAccent)), - child: Text('current user'))), - Visibility( - visible: _attendees[index].isOrganiser, + child: Text( + 'Android: ${_attendees[index].androidAttendeeDetails?.attendanceStatus?.enumToString}')), + ), + Visibility( + visible: + _attendees[index].iosAttendeeDetails != + null, child: Container( margin: const EdgeInsets.symmetric( vertical: 10.0), @@ -489,406 +470,431 @@ class _CalendarEventPageState extends State { decoration: BoxDecoration( border: Border.all( color: Colors.blueAccent)), - child: Text('Organiser'))), - Container( - margin: const EdgeInsets.symmetric( - vertical: 10.0), - padding: const EdgeInsets.all(3.0), - decoration: BoxDecoration( - border: - Border.all(color: Colors.blueAccent)), - child: Text( - '${_attendees[index].role?.enumToString}'), - ), - IconButton( - padding: const EdgeInsets.all(0), - onPressed: () { - setState(() { - _attendees.removeAt(index); - }); - }, - icon: Icon( - Icons.remove_circle, - color: Colors.redAccent, + child: Text( + 'iOS: ${_attendees[index].iosAttendeeDetails?.attendanceStatus?.enumToString}')), + ), + Visibility( + visible: _attendees[index].isCurrentUser, + child: Container( + margin: const EdgeInsets.symmetric( + vertical: 10.0), + padding: const EdgeInsets.all(3.0), + decoration: BoxDecoration( + border: Border.all( + color: Colors.blueAccent)), + child: const Text('current user'))), + Visibility( + visible: _attendees[index].isOrganiser, + child: Container( + margin: const EdgeInsets.symmetric( + vertical: 10.0), + padding: const EdgeInsets.all(3.0), + decoration: BoxDecoration( + border: Border.all( + color: Colors.blueAccent)), + child: const Text('Organiser'))), + Container( + margin: const EdgeInsets.symmetric( + vertical: 10.0), + padding: const EdgeInsets.all(3.0), + decoration: BoxDecoration( + border: Border.all( + color: Colors.blueAccent)), + child: Text( + '${_attendees[index].role?.enumToString}'), ), - ) + IconButton( + padding: const EdgeInsets.all(0), + onPressed: () { + setState(() { + _attendees.removeAt(index); + }); + }, + icon: const Icon( + Icons.remove_circle, + color: Colors.redAccent, + ), + ) + ], + ), + ), + ); + }, + ), + GestureDetector( + onTap: () async { + var result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + EventRemindersPage(_reminders))); + if (result == null) { + return; + } + _reminders = result; + }, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Align( + alignment: Alignment.centerLeft, + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 10.0, + children: [ + const Icon(Icons.alarm), + if (_reminders.isEmpty) + Text(_calendar.isReadOnly == false + ? 'Add reminders' + : 'Reminders'), + for (var reminder in _reminders) + Text('${reminder.minutes} minutes before; ') ], ), ), - ); - }, - ), - GestureDetector( - onTap: () async { - var result = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - EventRemindersPage(_reminders))); - if (result == null) { - return; - } - _reminders = result; - }, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Align( - alignment: Alignment.centerLeft, - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 10.0, - children: [ - Icon(Icons.alarm), - if (_reminders.isEmpty) - Text(_calendar.isReadOnly == false - ? 'Add reminders' - : 'Reminders'), - for (var reminder in _reminders) - Text('${reminder.minutes} minutes before; ') - ], - ), ), ), - ), - CheckboxListTile( - value: _isRecurringEvent, - title: Text('Is recurring'), - onChanged: (isChecked) { - setState(() { - _isRecurringEvent = isChecked ?? false; - }); - }, - ), - if (_isRecurringEvent) ...[ - ListTile( - leading: Text('Select a Recurrence Type'), - trailing: DropdownButton( - onChanged: (selectedFrequency) { - setState(() { - _recurrenceFrequency = selectedFrequency; - _getValidDaysOfMonth(_recurrenceFrequency); - }); - }, - value: _recurrenceFrequency, - items: RecurrenceFrequency.values - .map((frequency) => DropdownMenuItem( - value: frequency, - child: - _recurrenceFrequencyToText(frequency), - )) - .toList(), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(15, 0, 15, 10), - child: Row( - children: [ - Text('Repeat Every '), - Flexible( - child: TextFormField( - initialValue: _interval?.toString() ?? '1', - decoration: - const InputDecoration(hintText: '1'), - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - LengthLimitingTextInputFormatter(2) - ], - validator: _validateInterval, - textAlign: TextAlign.right, - onSaved: (String? value) { - if (value != null) { - _interval = int.tryParse(value); - } - }, - ), - ), - _recurrenceFrequencyToIntervalText( - _recurrenceFrequency), - ], - ), + CheckboxListTile( + value: _isRecurringEvent, + title: const Text('Is recurring'), + onChanged: (isChecked) { + setState(() { + _isRecurringEvent = isChecked ?? false; + }); + }, ), - if (_recurrenceFrequency == - RecurrenceFrequency.Weekly) ...[ - Column( - children: [ - ...DayOfWeek.values.map((day) { - return CheckboxListTile( - title: Text(day.enumToString), - value: _daysOfWeek.any((dow) => dow == day), - onChanged: (selected) { - setState(() { - if (selected == true) { - _daysOfWeek.add(day); - } else { - _daysOfWeek.remove(day); - } - _updateDaysOfWeekGroup(selectedDay: day); - }); - }, - ); - }), - Divider(color: Colors.black), - ...DayOfWeekGroup.values.map((group) { - return RadioListTile( - title: Text(group.enumToString), - value: group, - groupValue: _dayOfWeekGroup, - onChanged: (selected) { - setState(() { - _dayOfWeekGroup = - selected as DayOfWeekGroup; - _updateDaysOfWeek(); - }); - }, - controlAffinity: - ListTileControlAffinity.trailing); - }), - ], - ) - ], - if (_recurrenceFrequency == RecurrenceFrequency.Monthly || - _recurrenceFrequency == - RecurrenceFrequency.Yearly) ...[ - SwitchListTile( - value: _isByDayOfMonth, - onChanged: (value) => - setState(() => _isByDayOfMonth = value), - title: Text('By day of the month'), - ) - ], - if (_recurrenceFrequency == RecurrenceFrequency.Yearly && - _isByDayOfMonth) ...[ + if (_isRecurringEvent) ...[ ListTile( - leading: Text('Month of the year'), - trailing: DropdownButton( - onChanged: (value) { + leading: const Text('Select a Recurrence Type'), + trailing: DropdownButton( + onChanged: (selectedFrequency) { setState(() { - _monthOfYear = value; + _recurrenceFrequency = selectedFrequency; _getValidDaysOfMonth(_recurrenceFrequency); }); }, - value: _monthOfYear, - items: MonthOfYear.values - .map((month) => DropdownMenuItem( - value: month, - child: Text(month.enumToString), - )) - .toList(), - ), - ), - ], - if (_isByDayOfMonth && - (_recurrenceFrequency == - RecurrenceFrequency.Monthly || - _recurrenceFrequency == - RecurrenceFrequency.Yearly)) ...[ - ListTile( - leading: Text('Day of the month'), - trailing: DropdownButton( - onChanged: (value) { - setState(() { - _dayOfMonth = value; - }); - }, - value: _dayOfMonth, - items: _validDaysOfMonth - .map((day) => DropdownMenuItem( - value: day, - child: Text(day.toString()), + value: _recurrenceFrequency, + items: RecurrenceFrequency.values + .map((frequency) => DropdownMenuItem( + value: frequency, + child: + _recurrenceFrequencyToText(frequency), )) .toList(), ), ), - ], - if (!_isByDayOfMonth && - (_recurrenceFrequency == - RecurrenceFrequency.Monthly || - _recurrenceFrequency == - RecurrenceFrequency.Yearly)) ...[ - Padding( - padding: const EdgeInsets.fromLTRB(15, 10, 15, 10), - child: Align( - alignment: Alignment.centerLeft, - child: _recurrenceFrequencyToText( - _recurrenceFrequency) - .data != - null - ? Text(_recurrenceFrequencyToText( - _recurrenceFrequency) - .data! + - ' on the ') - : Text('')), - ), Padding( padding: const EdgeInsets.fromLTRB(15, 0, 15, 10), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Flexible( - child: DropdownButton( - onChanged: (value) { - setState(() { - _weekOfMonth = value; - }); - }, - value: _weekOfMonth ?? WeekNumber.First, - items: WeekNumber.values - .map((weekNum) => DropdownMenuItem( - value: weekNum, - child: Text(weekNum.enumToString), - )) - .toList(), - ), - ), - Flexible( - child: DropdownButton( - onChanged: (value) { - setState(() { - _selectedDayOfWeek = value; - }); - }, - value: _selectedDayOfWeek != null - ? DayOfWeek - .values[_selectedDayOfWeek!.index] - : DayOfWeek.values[0], - items: DayOfWeek.values - .map((day) => DropdownMenuItem( - value: day, - child: Text(day.enumToString), - )) - .toList(), - ), - ), - if (_recurrenceFrequency == - RecurrenceFrequency.Yearly) ...[ - Text('of'), - Flexible( - child: DropdownButton( - onChanged: (value) { - setState(() { - _monthOfYear = value; - }); - }, - value: _monthOfYear, - items: MonthOfYear.values - .map((month) => DropdownMenuItem( - value: month, - child: Text(month.enumToString), - )) - .toList(), - ), - ), - ] - ], - ), - ), - ], - ListTile( - leading: Text('Event ends'), - trailing: DropdownButton( - onChanged: (value) { - setState(() { - _recurrenceRuleEndType = value; - }); - }, - value: _recurrenceRuleEndType, - items: RecurrenceRuleEndType.values - .map((frequency) => DropdownMenuItem( - value: frequency, - child: - _recurrenceRuleEndTypeToText(frequency), - )) - .toList(), - ), - ), - if (_recurrenceRuleEndType == - RecurrenceRuleEndType.MaxOccurrences) - Padding( - padding: const EdgeInsets.fromLTRB(15, 0, 15, 10), - child: Row( - children: [ - Text('For the next '), + const Text('Repeat Every '), Flexible( child: TextFormField( - initialValue: - _totalOccurrences?.toString() ?? '1', + initialValue: _interval?.toString() ?? '1', decoration: const InputDecoration(hintText: '1'), keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, - LengthLimitingTextInputFormatter(3), + LengthLimitingTextInputFormatter(2) ], - validator: _validateTotalOccurrences, + validator: _validateInterval, textAlign: TextAlign.right, onSaved: (String? value) { if (value != null) { - _totalOccurrences = int.tryParse(value); + _interval = int.tryParse(value); } }, ), ), - Text(' occurrences'), + _recurrenceFrequencyToIntervalText( + _recurrenceFrequency), ], ), ), - if (_recurrenceRuleEndType == - RecurrenceRuleEndType.SpecifiedEndDate) - Padding( - padding: const EdgeInsets.all(10.0), - child: DateTimePicker( - labelText: 'Date', - enableTime: false, - selectedDate: _recurrenceEndDate, - selectDate: (DateTime date) { + if (_recurrenceFrequency == + RecurrenceFrequency.Weekly) ...[ + Column( + children: [ + ...DayOfWeek.values.map((day) { + return CheckboxListTile( + title: Text(day.enumToString), + value: _daysOfWeek.any((dow) => dow == day), + onChanged: (selected) { + setState(() { + if (selected == true) { + _daysOfWeek.add(day); + } else { + _daysOfWeek.remove(day); + } + _updateDaysOfWeekGroup(selectedDay: day); + }); + }, + ); + }), + const Divider(color: Colors.black), + ...DayOfWeekGroup.values.map((group) { + return RadioListTile( + title: Text(group.enumToString), + value: group, + groupValue: _dayOfWeekGroup, + onChanged: (selected) { + setState(() { + _dayOfWeekGroup = + selected as DayOfWeekGroup; + _updateDaysOfWeek(); + }); + }, + controlAffinity: + ListTileControlAffinity.trailing); + }), + ], + ) + ], + if (_recurrenceFrequency == + RecurrenceFrequency.Monthly || + _recurrenceFrequency == + RecurrenceFrequency.Yearly) ...[ + SwitchListTile( + value: _isByDayOfMonth, + onChanged: (value) => + setState(() => _isByDayOfMonth = value), + title: const Text('By day of the month'), + ) + ], + if (_recurrenceFrequency == + RecurrenceFrequency.Yearly && + _isByDayOfMonth) ...[ + ListTile( + leading: const Text('Month of the year'), + trailing: DropdownButton( + onChanged: (value) { + setState(() { + _monthOfYear = value; + _getValidDaysOfMonth(_recurrenceFrequency); + }); + }, + value: _monthOfYear, + items: MonthOfYear.values + .map((month) => DropdownMenuItem( + value: month, + child: Text(month.enumToString), + )) + .toList(), + ), + ), + ], + if (_isByDayOfMonth && + (_recurrenceFrequency == + RecurrenceFrequency.Monthly || + _recurrenceFrequency == + RecurrenceFrequency.Yearly)) ...[ + ListTile( + leading: const Text('Day of the month'), + trailing: DropdownButton( + onChanged: (value) { + setState(() { + _dayOfMonth = value; + }); + }, + value: _dayOfMonth, + items: _validDaysOfMonth + .map((day) => DropdownMenuItem( + value: day, + child: Text(day.toString()), + )) + .toList(), + ), + ), + ], + if (!_isByDayOfMonth && + (_recurrenceFrequency == + RecurrenceFrequency.Monthly || + _recurrenceFrequency == + RecurrenceFrequency.Yearly)) ...[ + Padding( + padding: const EdgeInsets.fromLTRB(15, 10, 15, 10), + child: Align( + alignment: Alignment.centerLeft, + child: _recurrenceFrequencyToText( + _recurrenceFrequency) + .data != + null + ? Text(_recurrenceFrequencyToText( + _recurrenceFrequency) + .data! + + ' on the ') + : const Text('')), + ), + Padding( + padding: const EdgeInsets.fromLTRB(15, 0, 15, 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: DropdownButton( + onChanged: (value) { + setState(() { + _weekOfMonth = value; + }); + }, + value: _weekOfMonth ?? WeekNumber.First, + items: WeekNumber.values + .map((weekNum) => DropdownMenuItem( + value: weekNum, + child: Text(weekNum.enumToString), + )) + .toList(), + ), + ), + Flexible( + child: DropdownButton( + onChanged: (value) { + setState(() { + _selectedDayOfWeek = value; + }); + }, + value: _selectedDayOfWeek != null + ? DayOfWeek + .values[_selectedDayOfWeek!.index] + : DayOfWeek.values[0], + items: DayOfWeek.values + .map((day) => DropdownMenuItem( + value: day, + child: Text(day.enumToString), + )) + .toList(), + ), + ), + if (_recurrenceFrequency == + RecurrenceFrequency.Yearly) ...[ + const Text('of'), + Flexible( + child: DropdownButton( + onChanged: (value) { + setState(() { + _monthOfYear = value; + }); + }, + value: _monthOfYear, + items: MonthOfYear.values + .map((month) => DropdownMenuItem( + value: month, + child: Text(month.enumToString), + )) + .toList(), + ), + ), + ] + ], + ), + ), + ], + ListTile( + leading: const Text('Event ends'), + trailing: DropdownButton( + onChanged: (value) { setState(() { - _recurrenceEndDate = date; + _recurrenceRuleEndType = value; }); }, + value: _recurrenceRuleEndType, + items: RecurrenceRuleEndType.values + .map((frequency) => DropdownMenuItem( + value: frequency, + child: _recurrenceRuleEndTypeToText( + frequency), + )) + .toList(), ), ), + if (_recurrenceRuleEndType == + RecurrenceRuleEndType.MaxOccurrences) + Padding( + padding: const EdgeInsets.fromLTRB(15, 0, 15, 10), + child: Row( + children: [ + const Text('For the next '), + Flexible( + child: TextFormField( + initialValue: + _totalOccurrences?.toString() ?? '1', + decoration: + const InputDecoration(hintText: '1'), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(3), + ], + validator: _validateTotalOccurrences, + textAlign: TextAlign.right, + onSaved: (String? value) { + if (value != null) { + _totalOccurrences = int.tryParse(value); + } + }, + ), + ), + const Text(' occurrences'), + ], + ), + ), + if (_recurrenceRuleEndType == + RecurrenceRuleEndType.SpecifiedEndDate) + Padding( + padding: const EdgeInsets.all(10.0), + child: DateTimePicker( + labelText: 'Date', + enableTime: false, + selectedDate: _recurrenceEndDate, + selectDate: (DateTime date) { + setState(() { + _recurrenceEndDate = date; + }); + }, + ), + ), + ], ], - ], + ), ), - ), - if (_calendar.isReadOnly == false && - (_event?.eventId?.isNotEmpty ?? false)) ...[ - ElevatedButton( - key: Key('deleteEventButton'), - style: ElevatedButton.styleFrom( - primary: Colors.red, onPrimary: Colors.white), - onPressed: () async { - bool? result = true; - if (!_isRecurringEvent) { - await _deviceCalendarPlugin.deleteEvent( - _calendar.id, _event?.eventId); - } else { - result = await showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return _recurringEventDialog != null - ? _recurringEventDialog as Widget - : SizedBox(); - }); - } + if (_calendar.isReadOnly == false && + (_event?.eventId?.isNotEmpty ?? false)) ...[ + ElevatedButton( + key: const Key('deleteEventButton'), + style: ElevatedButton.styleFrom( + primary: Colors.red, onPrimary: Colors.white), + onPressed: () async { + bool? result = true; + if (!_isRecurringEvent) { + await _deviceCalendarPlugin.deleteEvent( + _calendar.id, _event?.eventId); + } else { + result = await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return _recurringEventDialog != null + ? _recurringEventDialog as Widget + : const SizedBox(); + }); + } - if (result == true) { - Navigator.pop(context, true); - } - }, - child: Text('Delete'), - ), - ] - ], + if (result == true) { + Navigator.pop(context, true); + } + }, + child: const Text('Delete'), + ), + ] + ], + ), ), ), ), floatingActionButton: Visibility( visible: _calendar.isReadOnly == false, child: FloatingActionButton( - key: Key('saveEventButton'), + key: const Key('saveEventButton'), onPressed: () async { final form = _formKey.currentState; if (form?.validate() == false) { @@ -940,7 +946,7 @@ class _CalendarEventPageState extends State { } } }, - child: Icon(Icons.check), + child: const Icon(Icons.check), ), ), ); @@ -949,15 +955,15 @@ class _CalendarEventPageState extends State { Text _recurrenceFrequencyToText(RecurrenceFrequency? recurrenceFrequency) { switch (recurrenceFrequency) { case RecurrenceFrequency.Daily: - return Text('Daily'); + return const Text('Daily'); case RecurrenceFrequency.Weekly: - return Text('Weekly'); + return const Text('Weekly'); case RecurrenceFrequency.Monthly: - return Text('Monthly'); + return const Text('Monthly'); case RecurrenceFrequency.Yearly: - return Text('Yearly'); + return const Text('Yearly'); default: - return Text(''); + return const Text(''); } } @@ -965,28 +971,28 @@ class _CalendarEventPageState extends State { RecurrenceFrequency? recurrenceFrequency) { switch (recurrenceFrequency) { case RecurrenceFrequency.Daily: - return Text(' Day(s)'); + return const Text(' Day(s)'); case RecurrenceFrequency.Weekly: - return Text(' Week(s) on'); + return const Text(' Week(s) on'); case RecurrenceFrequency.Monthly: - return Text(' Month(s)'); + return const Text(' Month(s)'); case RecurrenceFrequency.Yearly: - return Text(' Year(s)'); + return const Text(' Year(s)'); default: - return Text(''); + return const Text(''); } } Text _recurrenceRuleEndTypeToText(RecurrenceRuleEndType endType) { switch (endType) { case RecurrenceRuleEndType.Indefinite: - return Text('Indefinitely'); + return const Text('Indefinitely'); case RecurrenceRuleEndType.MaxOccurrences: - return Text('After a set number of times'); + return const Text('After a set number of times'); case RecurrenceRuleEndType.SpecifiedEndDate: - return Text('Continues until a specified date'); + return const Text('Continues until a specified date'); default: - return Text(''); + return const Text(''); } } diff --git a/example/lib/presentation/pages/calendar_events.dart b/example/lib/presentation/pages/calendar_events.dart index 0cd6f182..f0b6c27e 100644 --- a/example/lib/presentation/pages/calendar_events.dart +++ b/example/lib/presentation/pages/calendar_events.dart @@ -61,19 +61,19 @@ class _CalendarEventsPageState extends State { }, ), if (_isLoading) - Center( + const Center( child: CircularProgressIndicator(), ) ], ) - : Center(child: Text('No events found')), + : const Center(child: Text('No events found')), floatingActionButton: _getAddEventButton(context)); } Widget? _getAddEventButton(BuildContext context) { if (_calendar.isReadOnly == false || _calendar.isReadOnly == null) { return FloatingActionButton( - key: Key('addEventButton'), + key: const Key('addEventButton'), onPressed: () async { final refreshEvents = await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { @@ -83,7 +83,7 @@ class _CalendarEventsPageState extends State { await _retrieveCalendarEvents(); } }, - child: Icon(Icons.add), + child: const Icon(Icons.add), ); } else { return null; @@ -100,7 +100,7 @@ class _CalendarEventsPageState extends State { if (deleteSucceeded) { await _retrieveCalendarEvents(); } else { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Oops, we ran into an issue deleting the event'), backgroundColor: Colors.red, duration: Duration(seconds: 5), @@ -131,8 +131,8 @@ class _CalendarEventsPageState extends State { } Future _retrieveCalendarEvents() async { - final startDate = DateTime.now().add(Duration(days: -30)); - final endDate = DateTime.now().add(Duration(days: 30)); + final startDate = DateTime.now().add(const Duration(days: -30)); + final endDate = DateTime.now().add(const Duration(days: 30)); var calendarEventsResult = await _deviceCalendarPlugin.retrieveEvents( _calendar.id, RetrieveEventsParams(startDate: startDate, endDate: endDate)); @@ -144,7 +144,7 @@ class _CalendarEventsPageState extends State { Widget _getDeleteButton() { return IconButton( - icon: Icon(Icons.delete), + icon: const Icon(Icons.delete), onPressed: () async { await _showDeleteDialog(); }); @@ -155,12 +155,12 @@ class _CalendarEventsPageState extends State { context: context, builder: (BuildContext context) { return AlertDialog( - title: Text('Warning'), + title: const Text('Warning'), content: SingleChildScrollView( child: ListBody( children: [ - Text('This will delete this calendar'), - Text('Are you sure?'), + const Text('This will delete this calendar'), + const Text('Are you sure?'), ], ), ), @@ -174,13 +174,13 @@ class _CalendarEventsPageState extends State { Navigator.of(context).pop(); Navigator.of(context).pop(); }, - child: Text('Delete!'), + child: const Text('Delete!'), ), TextButton( onPressed: () { Navigator.of(context).pop(); }, - child: Text('Cancel'), + child: const Text('Cancel'), ), ], ); diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index 98601e14..b09596fe 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'calendar_events.dart'; class CalendarsPage extends StatefulWidget { - CalendarsPage({Key? key}) : super(key: key); + const CalendarsPage({Key? key}) : super(key: key); @override _CalendarsPageState createState() { @@ -37,7 +37,7 @@ class _CalendarsPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('Calendars'), + title: const Text('Calendars'), actions: [_getRefreshButton()], ), body: Column( @@ -62,7 +62,7 @@ class _CalendarsPageState extends State { await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { return CalendarEventsPage(_calendars[index], - key: Key('calendarEventsPage')); + key: const Key('calendarEventsPage')); })); }, child: Padding( @@ -91,14 +91,14 @@ class _CalendarsPageState extends State { shape: BoxShape.circle, color: Color(_calendars[index].color!)), ), - SizedBox(width: 10), + const SizedBox(width: 10), if (_calendars[index].isDefault!) Container( margin: const EdgeInsets.fromLTRB(0, 0, 5.0, 0), padding: const EdgeInsets.all(3.0), decoration: BoxDecoration( border: Border.all(color: Colors.blueAccent)), - child: Text('Default'), + child: const Text('Default'), ), Icon(_calendars[index].isReadOnly == true ? Icons.lock @@ -123,7 +123,7 @@ class _CalendarsPageState extends State { _retrieveCalendars(); } }, - child: Icon(Icons.add), + child: const Icon(Icons.add), ), ); } @@ -153,7 +153,7 @@ class _CalendarsPageState extends State { Widget _getRefreshButton() { return IconButton( - icon: Icon(Icons.refresh), + icon: const Icon(Icons.refresh), onPressed: () async { _retrieveCalendars(); }); diff --git a/example/lib/presentation/pages/event_attendee.dart b/example/lib/presentation/pages/event_attendee.dart index 002ba6eb..a2f17aba 100644 --- a/example/lib/presentation/pages/event_attendee.dart +++ b/example/lib/presentation/pages/event_attendee.dart @@ -90,7 +90,7 @@ class _EventAttendeePageState extends State { ), ), ListTile( - leading: Text('Role'), + leading: const Text('Role'), trailing: DropdownButton( onChanged: (value) { setState(() { @@ -118,14 +118,14 @@ class _EventAttendeePageState extends State { context, ModalRoute.withName(AppRoutes.calendars)); //TODO: finish calling and getting attendee details from iOS }, - leading: Icon(Icons.edit), - title: Text('View / edit iOS attendance details'), + leading: const Icon(Icons.edit), + title: const Text('View / edit iOS attendance details'), ), ), Visibility( visible: Platform.isAndroid, child: ListTile( - leading: Text('Android attendee status'), + leading: const Text('Android attendee status'), trailing: DropdownButton( onChanged: (value) { setState(() { diff --git a/example/lib/presentation/pages/event_reminders.dart b/example/lib/presentation/pages/event_reminders.dart index 11619285..4b0a11f3 100644 --- a/example/lib/presentation/pages/event_reminders.dart +++ b/example/lib/presentation/pages/event_reminders.dart @@ -3,7 +3,7 @@ import 'package:device_calendar/device_calendar.dart'; class EventRemindersPage extends StatefulWidget { final List _reminders; - EventRemindersPage(this._reminders, {Key? key}) : super(key: key); + const EventRemindersPage(this._reminders, {Key? key}) : super(key: key); @override _EventRemindersPageState createState() => @@ -29,7 +29,7 @@ class _EventRemindersPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('Reminders'), + title: const Text('Reminders'), ), body: Column( children: [ @@ -64,7 +64,7 @@ class _EventRemindersPageState extends State { }); } }, - child: Text('Add'), + child: const Text('Add'), ), ], ), @@ -83,7 +83,7 @@ class _EventRemindersPageState extends State { (a) => a.minutes == _reminders[index].minutes); }); }, - child: Text('Delete'), + child: const Text('Delete'), ), ); }, @@ -93,7 +93,7 @@ class _EventRemindersPageState extends State { onPressed: () { Navigator.pop(context, _reminders); }, - child: Text('Done'), + child: const Text('Done'), ) ], ), diff --git a/example/lib/presentation/recurring_event_dialog.dart b/example/lib/presentation/recurring_event_dialog.dart index b3c63b3d..c8b8ff35 100644 --- a/example/lib/presentation/recurring_event_dialog.dart +++ b/example/lib/presentation/recurring_event_dialog.dart @@ -8,7 +8,7 @@ class RecurringEventDialog extends StatefulWidget { final VoidCallback _onLoadingStarted; final Function(bool) _onDeleteFinished; - RecurringEventDialog(this._deviceCalendarPlugin, this._calendarEvent, + const RecurringEventDialog(this._deviceCalendarPlugin, this._calendarEvent, this._onLoadingStarted, this._onDeleteFinished, {Key? key}) : super(key: key); @@ -38,7 +38,7 @@ class _RecurringEventDialogState extends State { @override Widget build(BuildContext context) { return SimpleDialog( - title: Text('Are you sure you want to delete this event?'), + title: const Text('Are you sure you want to delete this event?'), children: [ SimpleDialogOption( onPressed: () async { @@ -56,7 +56,7 @@ class _RecurringEventDialogState extends State { deleteResult.isSuccess && deleteResult.data != null); } }, - child: Text('This instance only'), + child: const Text('This instance only'), ), SimpleDialogOption( onPressed: () async { @@ -74,7 +74,7 @@ class _RecurringEventDialogState extends State { deleteResult.isSuccess && deleteResult.data != null); } }, - child: Text('This and following instances'), + child: const Text('This and following instances'), ), SimpleDialogOption( onPressed: () async { @@ -87,13 +87,13 @@ class _RecurringEventDialogState extends State { deleteResult.isSuccess && deleteResult.data != null); } }, - child: Text('All instances'), + child: const Text('All instances'), ), SimpleDialogOption( onPressed: () { Navigator.of(context).pop(false); }, - child: Text('Cancel'), + child: const Text('Cancel'), ) ], ); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c6eeea43..e840c944 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,7 @@ name: device_calendar_example description: Demonstrates how to use the device_calendar plugin. version: 3.2.0 +publish_to: none environment: sdk: '>=2.12.0 <3.0.0' diff --git a/lib/src/common/error_messages.dart b/lib/src/common/error_messages.dart index dc05d425..f2126006 100644 --- a/lib/src/common/error_messages.dart +++ b/lib/src/common/error_messages.dart @@ -21,7 +21,7 @@ class ErrorMessages { static const String unknownDeviceIssue = 'Device calendar plugin ran into an unknown issue'; static const String unknownDeviceExceptionTemplate = - 'Device calendar plugin ran into an issue. Platform specific exception [%s], with message :\"%s\", has been thrown.'; + 'Device calendar plugin ran into an issue. Platform specific exception [%s], with message :"%s", has been thrown.'; static const String unknownDeviceGenericExceptionTemplate = - 'Device calendar plugin ran into an issue, with message \"%s\"'; + 'Device calendar plugin ran into an issue, with message "%s"'; } diff --git a/lib/src/device_calendar.dart b/lib/src/device_calendar.dart index eb1d9db1..728024c1 100644 --- a/lib/src/device_calendar.dart +++ b/lib/src/device_calendar.dart @@ -214,9 +214,9 @@ class DeviceCalendarPlugin { // allDay events on Android need to be at midnight UTC event.start = Platform.isAndroid ? TZDateTime.utc(event.start!.year, event.start!.month, - event.start!.day, 0, 0, 0) + event.start!.day, 0, 0, 0) : TZDateTime.from(dateStart, - timeZoneDatabase.locations[event.start!.location.name]!); + timeZoneDatabase.locations[event.start!.location.name]!); } if (event.end != null) { var dateEnd = DateTime( @@ -226,10 +226,10 @@ class DeviceCalendarPlugin { // Jan 1 and 2, should be from Jan 1 00:00:00 to Jan 3 00:00:00 event.end = Platform.isAndroid ? TZDateTime.utc(event.end!.year, event.end!.month, - event.end!.day, 0, 0, 0) - .add(Duration(days: 1)) + event.end!.day, 0, 0, 0) + .add(const Duration(days: 1)) : TZDateTime.from(dateEnd, - timeZoneDatabase.locations[event.end!.location.name]!); + timeZoneDatabase.locations[event.end!.location.name]!); } } @@ -322,7 +322,7 @@ class DeviceCalendarPlugin { /// Displays a native iOS view [EKEventViewController] /// https://developer.apple.com/documentation/eventkitui/ekeventviewcontroller - /// + /// /// Allows to change the event's attendance status /// Works only on iOS /// Returns after dismissing EKEventViewController's dialog @@ -337,7 +337,6 @@ class DeviceCalendarPlugin { ); } - Future> _invokeChannelMethod( String channelMethodName, { Function(Result)? assertParameters, @@ -375,7 +374,7 @@ class DeviceCalendarPlugin { Exception? exception, Result result) { if (exception == null) { result.errors.add( - ResultError( + const ResultError( ErrorCodes.unknown, ErrorMessages.unknownDeviceIssue, ), diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index bc3e2f2d..ff536688 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -138,7 +138,7 @@ class Event { end = end?.subtract(Duration(milliseconds: endOffset)); // The Event End Date for allDay events is midnight of the next day, so // subtract one day - end = end?.subtract(Duration(days: 1)); + end = end?.subtract(const Duration(days: 1)); } location = json['eventLocation']; availability = parseStringToAvailability(json['availability']); @@ -179,7 +179,7 @@ class Event { }).toList(); } if (legacyJSON) { - throw FormatException( + throw const FormatException( 'legacy JSON detected. Please update your current JSONs as they may not be supported later on.'); } } diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart index 6c105c0a..3889c44b 100644 --- a/test/device_calendar_test.dart +++ b/test/device_calendar_test.dart @@ -7,8 +7,7 @@ import 'package:timezone/timezone.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final channel = - const MethodChannel('plugins.builttoroam.com/device_calendar'); + const channel = MethodChannel('plugins.builttoroam.com/device_calendar'); var deviceCalendarPlugin = DeviceCalendarPlugin(); final log = []; @@ -47,9 +46,9 @@ void main() { }); test('RetrieveCalendars_Returns_Successfully', () async { - final fakeCalendarName = 'fakeCalendarName'; + const fakeCalendarName = 'fakeCalendarName'; channel.setMockMethodCallHandler((MethodCall methodCall) async { - return '[{\"id\":\"1\",\"isReadOnly\":false,\"name\":\"$fakeCalendarName\"}]'; + return '[{"id":"1","isReadOnly":false,"name":"$fakeCalendarName"}]'; }); final result = await deviceCalendarPlugin.retrieveCalendars(); @@ -61,8 +60,8 @@ void main() { }); test('RetrieveEvents_CalendarId_IsRequired', () async { - final String? calendarId = null; - final params = RetrieveEventsParams(); + const String? calendarId = null; + const params = RetrieveEventsParams(); final result = await deviceCalendarPlugin.retrieveEvents(calendarId, params); @@ -72,8 +71,8 @@ void main() { }); test('DeleteEvent_CalendarId_IsRequired', () async { - final String? calendarId = null; - final eventId = 'fakeEventId'; + const String? calendarId = null; + const eventId = 'fakeEventId'; final result = await deviceCalendarPlugin.deleteEvent(calendarId, eventId); expect(result.isSuccess, false); @@ -82,8 +81,8 @@ void main() { }); test('DeleteEvent_EventId_IsRequired', () async { - final calendarId = 'fakeCalendarId'; - final String? eventId = null; + const calendarId = 'fakeCalendarId'; + const String? eventId = null; final result = await deviceCalendarPlugin.deleteEvent(calendarId, eventId); expect(result.isSuccess, false); @@ -92,8 +91,8 @@ void main() { }); test('DeleteEvent_PassesArguments_Correctly', () async { - final calendarId = 'fakeCalendarId'; - final eventId = 'fakeEventId'; + const calendarId = 'fakeCalendarId'; + const eventId = 'fakeEventId'; await deviceCalendarPlugin.deleteEvent(calendarId, eventId); expect(log, [ @@ -105,7 +104,7 @@ void main() { }); test('CreateEvent_Arguments_Invalid', () async { - final String? fakeCalendarId = null; + const String? fakeCalendarId = null; final event = Event(fakeCalendarId); final result = await deviceCalendarPlugin.createOrUpdateEvent(event); @@ -115,16 +114,16 @@ void main() { }); test('CreateEvent_Returns_Successfully', () async { - final fakeNewEventId = 'fakeNewEventId'; + const fakeNewEventId = 'fakeNewEventId'; channel.setMockMethodCallHandler((MethodCall methodCall) async { return fakeNewEventId; }); - final fakeCalendarId = 'fakeCalendarId'; + const fakeCalendarId = 'fakeCalendarId'; final event = Event(fakeCalendarId); event.title = 'fakeEventTitle'; event.start = TZDateTime.now(local); - event.end = event.start!.add(Duration(hours: 1)); + event.end = event.start!.add(const Duration(hours: 1)); final result = await deviceCalendarPlugin.createOrUpdateEvent(event); expect(result?.isSuccess, true); @@ -134,7 +133,7 @@ void main() { }); test('UpdateEvent_Returns_Successfully', () async { - final fakeNewEventId = 'fakeNewEventId'; + const fakeNewEventId = 'fakeNewEventId'; channel.setMockMethodCallHandler((MethodCall methodCall) async { final arguments = methodCall.arguments as Map; if (!arguments.containsKey('eventId') || arguments['eventId'] == null) { @@ -144,12 +143,12 @@ void main() { return fakeNewEventId; }); - final fakeCalendarId = 'fakeCalendarId'; + const fakeCalendarId = 'fakeCalendarId'; final event = Event(fakeCalendarId); event.eventId = 'fakeEventId'; event.title = 'fakeEventTitle'; event.start = TZDateTime.now(local); - event.end = event.start!.add(Duration(hours: 1)); + event.end = event.start!.add(const Duration(hours: 1)); final result = await deviceCalendarPlugin.createOrUpdateEvent(event); expect(result?.isSuccess, true); From 9147125e7675be9a959a3d7cc2eda80ce2b4a76d Mon Sep 17 00:00:00 2001 From: thomassth Date: Thu, 6 Oct 2022 21:21:22 -0400 Subject: [PATCH 05/43] more linting fixes --- example/lib/main.dart | 2 +- example/lib/presentation/event_item.dart | 2 +- example/lib/presentation/pages/calendar_events.dart | 2 +- example/lib/presentation/pages/calendars.dart | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index ffb03586..3b5d61ee 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -9,7 +9,7 @@ class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override - _MyAppState createState() => _MyAppState(); + State createState() => _MyAppState(); } class _MyAppState extends State { diff --git a/example/lib/presentation/event_item.dart b/example/lib/presentation/event_item.dart index ec0eb425..d0fa800d 100644 --- a/example/lib/presentation/event_item.dart +++ b/example/lib/presentation/event_item.dart @@ -27,7 +27,7 @@ class EventItem extends StatefulWidget { : super(key: key); @override - _EventItemState createState() { + State createState() { return _EventItemState(); } } diff --git a/example/lib/presentation/pages/calendar_events.dart b/example/lib/presentation/pages/calendar_events.dart index f0b6c27e..584458e3 100644 --- a/example/lib/presentation/pages/calendar_events.dart +++ b/example/lib/presentation/pages/calendar_events.dart @@ -10,7 +10,7 @@ import 'calendar_event.dart'; class CalendarEventsPage extends StatefulWidget { final Calendar _calendar; - CalendarEventsPage(this._calendar, {Key? key}) : super(key: key); + const CalendarEventsPage(this._calendar, {Key? key}) : super(key: key); @override _CalendarEventsPageState createState() { diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index b09596fe..4dbf22e3 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -116,7 +116,7 @@ class _CalendarsPageState extends State { onPressed: () async { final createCalendar = await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { - return CalendarAddPage(); + return const CalendarAddPage(); })); if (createCalendar == true) { From 30a6a5fcce70c2cb30b25d50d3868d3a7929d1a2 Mon Sep 17 00:00:00 2001 From: Thomas Kam Date: Thu, 6 Oct 2022 23:58:59 -0400 Subject: [PATCH 06/43] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 36 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..ffebd121 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +In a few sentence, briefly describe of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +Optional, add screenshots if it helps explain your problem. + +**Device(s) tested** +This can be very important as not all device vendors do calendar in the same way. + - Device: [e.g. Pixel 6] + - OS: [e.g. Android 12.0] + - Plugin version [e.g. 4.2.0 Release] + +**Flutter doctor** +Run a `flutter doctor` so we can rule out env issues + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..11fc491e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 145013c29ea35dd697edb6dc50cbfe2eea6b1136 Mon Sep 17 00:00:00 2001 From: thomassth Date: Wed, 12 Oct 2022 01:31:00 -0400 Subject: [PATCH 07/43] packages cleanup --- example/lib/presentation/event_item.dart | 7 +++---- example/lib/presentation/pages/calendar_event.dart | 1 - lib/device_calendar.dart | 1 + lib/src/models/event.dart | 6 +++--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/example/lib/presentation/event_item.dart b/example/lib/presentation/event_item.dart index d0fa800d..ca401086 100644 --- a/example/lib/presentation/event_item.dart +++ b/example/lib/presentation/event_item.dart @@ -1,11 +1,11 @@ import 'dart:io'; + import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:intl/intl.dart'; import 'recurring_event_dialog.dart'; -import 'package:timezone/timezone.dart'; -import 'package:flutter_native_timezone/flutter_native_timezone.dart'; class EventItem extends StatefulWidget { final Event? _calendarEvent; @@ -222,8 +222,7 @@ class _EventItemState extends State { ), Expanded( child: Text( - widget._calendarEvent?.status?.enumToString ?? - '', + widget._calendarEvent?.status?.enumToString ?? '', overflow: TextOverflow.ellipsis, ), ) diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 587520ec..ff2a65ee 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:intl/intl.dart'; -import 'package:timezone/timezone.dart'; import '../date_time_picker.dart'; import '../recurring_event_dialog.dart'; diff --git a/lib/device_calendar.dart b/lib/device_calendar.dart index 15d51182..4811bb72 100644 --- a/lib/device_calendar.dart +++ b/lib/device_calendar.dart @@ -14,3 +14,4 @@ export 'src/models/platform_specifics/ios/attendance_status.dart'; export 'src/models/platform_specifics/android/attendee_details.dart'; export 'src/models/platform_specifics/android/attendance_status.dart'; export 'src/device_calendar.dart'; +export 'package:timezone/timezone.dart'; diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index ff536688..c7478005 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -1,9 +1,9 @@ import 'dart:io'; +import 'package:collection/collection.dart'; + import '../../device_calendar.dart'; import '../common/error_messages.dart'; -import 'package:timezone/timezone.dart'; -import 'package:collection/collection.dart'; /// An event associated with a calendar class Event { @@ -48,7 +48,7 @@ class Event { /// Indicates if this event is of confirmed, canceled, tentative or none status EventStatus? status; - + ///Note for development: /// ///JSON field names are coded in dart, swift and kotlin to facilitate data exchange. From c4c140c375506b36d015b3ff27661de829e0b2e4 Mon Sep 17 00:00:00 2001 From: Oleksandra Fedotova Date: Thu, 13 Oct 2022 12:11:52 +0300 Subject: [PATCH 08/43] Fix create/update events in IOS Fixed fullDay with DateRange events creation (not only one fullday events possible to create) Timezone should be set only if event is not full day --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 221 ++++++++++---------- 1 file changed, 110 insertions(+), 111 deletions(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index 59730a93..940a86b4 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -24,7 +24,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let accountName: String let accountType: String } - + struct Event: Codable { let eventId: String let calendarId: String @@ -43,7 +43,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let availability: Availability? let eventStatus: EventStatus? } - + struct RecurrenceRule: Codable { let recurrenceFrequency: Int let totalOccurrences: Int? @@ -54,7 +54,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let monthOfYear: Int? let weekOfMonth: Int? } - + struct Attendee: Codable { let name: String? let emailAddress: String @@ -62,11 +62,11 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let attendanceStatus: Int let isCurrentUser: Bool } - + struct Reminder: Codable { let minutes: Int } - + enum Availability: String, Codable { case BUSY case FREE @@ -80,7 +80,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele case CANCELED case NONE } - + static let channelName = "plugins.builttoroam.com/device_calendar" let notFoundErrorCode = "404" let notAllowed = "405" @@ -144,7 +144,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let instance = SwiftDeviceCalendarPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } - + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case requestPermissionsMethod: @@ -172,12 +172,12 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele result(FlutterMethodNotImplemented) } } - + private func hasPermissions(_ result: FlutterResult) { let hasPermissions = hasEventPermissions() result(hasPermissions) } - + private func getSource() -> EKSource? { let localSources = eventStore.sources.filter { $0.sourceType == .local } @@ -204,14 +204,14 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele do { calendar.title = arguments[calendarNameArgument] as! String let calendarColor = arguments[calendarColorArgument] as? String - + if (calendarColor != nil) { calendar.cgColor = UIColor(hex: calendarColor!)?.cgColor } else { calendar.cgColor = UIColor(red: 255, green: 0, blue: 0, alpha: 0).cgColor // Red colour as a default } - + guard let source = getSource() else { result(FlutterError(code: self.genericError, message: "Local calendar was not found.", details: nil)) return @@ -227,7 +227,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) } } - + private func retrieveCalendars(_ result: @escaping FlutterResult) { checkPermissionsThenExecute(permissionsGrantedAction: { let ekCalendars = self.eventStore.calendars(for: .event) @@ -244,27 +244,27 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele accountType: getAccountType(ekCalendar.source.sourceType)) calendars.append(calendar) } - + self.encodeJsonAndFinish(codable: calendars, result: result) }, result: result) } - + private func deleteCalendar(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { checkPermissionsThenExecute(permissionsGrantedAction: { let arguments = call.arguments as! Dictionary let calendarId = arguments[calendarIdArgument] as! String - + let ekCalendar = self.eventStore.calendar(withIdentifier: calendarId) if ekCalendar == nil { self.finishWithCalendarNotFoundError(result: result, calendarId: calendarId) return } - + if !(ekCalendar!.allowsContentModifications) { self.finishWithCalendarReadOnlyError(result: result, calendarId: calendarId) return } - + do { try self.eventStore.removeCalendar(ekCalendar!, commit: true) result(true) @@ -274,7 +274,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } }, result: result) } - + private func getAccountType(_ sourceType: EKSourceType) -> String { switch (sourceType) { case .local: @@ -293,7 +293,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele return "Unknown"; } } - + private func retrieveEvents(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { checkPermissionsThenExecute(permissionsGrantedAction: { let arguments = call.arguments as! Dictionary @@ -319,35 +319,35 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } } } - + guard let eventIds = eventIdArgs else { self.encodeJsonAndFinish(codable: events, result: result) return } - + if specifiedStartEndDates { events = events.filter({ (e) -> Bool in e.calendarId == calendarId && eventIds.contains(e.eventId) }) - + self.encodeJsonAndFinish(codable: events, result: result) return } - + for eventId in eventIds { let ekEvent = self.eventStore.event(withIdentifier: eventId) if ekEvent == nil { continue } - + let event = createEventFromEkEvent(calendarId: calendarId, ekEvent: ekEvent!) events.append(event) } - + self.encodeJsonAndFinish(codable: events, result: result) }, result: result) } - + private func createEventFromEkEvent(calendarId: String, ekEvent: EKEvent) -> Event { var attendees = [Attendee]() if ekEvent.attendees != nil { @@ -356,18 +356,18 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele if attendee == nil { continue } - + attendees.append(attendee!) } } - + var reminders = [Reminder]() if ekEvent.alarms != nil { for alarm in ekEvent.alarms! { reminders.append(Reminder(minutes: Int(-alarm.relativeOffset / 60))) } } - + let recurrenceRule = parseEKRecurrenceRules(ekEvent) let event = Event( eventId: ekEvent.eventIdentifier, @@ -390,12 +390,12 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele return event } - + private func convertEkParticipantToAttendee(ekParticipant: EKParticipant?) -> Attendee? { if ekParticipant == nil || ekParticipant?.emailAddress == nil { return nil } - + let attendee = Attendee( name: ekParticipant!.name, emailAddress: ekParticipant!.emailAddress!, @@ -403,10 +403,10 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele attendanceStatus: ekParticipant!.participantStatus.rawValue, isCurrentUser: ekParticipant!.isCurrentUser ) - + return attendee } - + private func convertEkEventAvailability(ekEventAvailability: EKEventAvailability?) -> Availability? { switch ekEventAvailability { case .busy: @@ -454,32 +454,32 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele default: frequency = 0 } - + var totalOccurrences: Int? var endDate: Int64? if(ekRecurrenceRule.recurrenceEnd?.occurrenceCount != nil && ekRecurrenceRule.recurrenceEnd?.occurrenceCount != 0) { totalOccurrences = ekRecurrenceRule.recurrenceEnd?.occurrenceCount } - + let endDateMs = ekRecurrenceRule.recurrenceEnd?.endDate?.millisecondsSinceEpoch if(endDateMs != nil) { endDate = Int64(exactly: endDateMs!) } - + var weekOfMonth = ekRecurrenceRule.setPositions?.first?.intValue - + var daysOfWeek: [Int]? if ekRecurrenceRule.daysOfTheWeek != nil && !ekRecurrenceRule.daysOfTheWeek!.isEmpty { daysOfWeek = [] for dayOfWeek in ekRecurrenceRule.daysOfTheWeek! { daysOfWeek!.append(dayOfWeek.dayOfTheWeek.rawValue - 1) - + if weekOfMonth == nil { weekOfMonth = dayOfWeek.weekNumber } } } - + // For recurrence of nth day of nth month every year, no calendar parameters are given // So we need to explicitly set them from event start date var dayOfMonth = ekRecurrenceRule.daysOfTheMonth?.first?.intValue @@ -487,16 +487,16 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele if (ekRecurrenceRule.frequency == EKRecurrenceFrequency.yearly && weekOfMonth == nil && dayOfMonth == nil && monthOfYear == nil) { let dateFormatter = DateFormatter() - + // Setting day of the month dateFormatter.dateFormat = "d" dayOfMonth = Int(dateFormatter.string(from: ekEvent.startDate)) - + // Setting month of the year dateFormatter.dateFormat = "M" monthOfYear = Int(dateFormatter.string(from: ekEvent.startDate)) } - + recurrenceRule = RecurrenceRule( recurrenceFrequency: frequency, totalOccurrences: totalOccurrences, @@ -507,37 +507,37 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele monthOfYear: monthOfYear, weekOfMonth: weekOfMonth) } - + return recurrenceRule } - + private func createEKRecurrenceRules(_ arguments: [String : AnyObject]) -> [EKRecurrenceRule]?{ let recurrenceRuleArguments = arguments[recurrenceRuleArgument] as? Dictionary if recurrenceRuleArguments == nil { return nil } - + let recurrenceFrequencyIndex = recurrenceRuleArguments![recurrenceFrequencyArgument] as? NSInteger let totalOccurrences = recurrenceRuleArguments![totalOccurrencesArgument] as? NSInteger let interval = recurrenceRuleArguments![intervalArgument] as? NSInteger var recurrenceInterval = 1 let endDate = recurrenceRuleArguments![endDateArgument] as? NSNumber let namedFrequency = validFrequencyTypes[recurrenceFrequencyIndex!] - + var recurrenceEnd:EKRecurrenceEnd? if endDate != nil { recurrenceEnd = EKRecurrenceEnd(end: Date.init(timeIntervalSince1970: endDate!.doubleValue / 1000)) } else if(totalOccurrences != nil && totalOccurrences! > 0) { recurrenceEnd = EKRecurrenceEnd(occurrenceCount: totalOccurrences!) } - + if interval != nil && interval! > 1 { recurrenceInterval = interval! } - + let daysOfWeekIndices = recurrenceRuleArguments![daysOfWeekArgument] as? [Int] var daysOfWeek : [EKRecurrenceDayOfWeek]? - + if daysOfWeekIndices != nil && !daysOfWeekIndices!.isEmpty { daysOfWeek = [] for dayOfWeekIndex in daysOfWeekIndices! { @@ -554,19 +554,19 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } } } - + var dayOfMonthArray : [NSNumber]? if let dayOfMonth = recurrenceRuleArguments![dayOfMonthArgument] as? Int { dayOfMonthArray = [] dayOfMonthArray!.append(NSNumber(value: dayOfMonth)) } - + var monthOfYearArray : [NSNumber]? if let monthOfYear = recurrenceRuleArguments![monthOfYearArgument] as? Int { monthOfYearArray = [] monthOfYearArray!.append(NSNumber(value: monthOfYear)) } - + // Append BYSETPOS only on monthly (but not last), yearly's week number (and last for monthly) appends to BYDAY var weekOfMonthArray : [NSNumber]? if namedFrequency == EKRecurrenceFrequency.monthly { @@ -577,7 +577,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } } } - + return [EKRecurrenceRule( recurrenceWith: namedFrequency, interval: recurrenceInterval, @@ -589,19 +589,19 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele setPositions: weekOfMonthArray, end: recurrenceEnd)] } - + private func setAttendees(_ arguments: [String : AnyObject], _ ekEvent: EKEvent?) { let attendeesArguments = arguments[attendeesArgument] as? [Dictionary] if attendeesArguments == nil { return } - + var attendees = [EKParticipant]() for attendeeArguments in attendeesArguments! { let name = attendeeArguments[nameArgument] as! String let emailAddress = attendeeArguments[emailAddressArgument] as! String let role = attendeeArguments[roleArgument] as! Int - + if (ekEvent!.attendees != nil) { let existingAttendee = ekEvent!.attendees!.first { element in return element.emailAddress == emailAddress @@ -611,40 +611,40 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele continue } } - + let attendee = createParticipant( name: name, emailAddress: emailAddress, role: role) - + if (attendee == nil) { continue } - + attendees.append(attendee!) } - + ekEvent!.setValue(attendees, forKey: "attendees") } - + private func createReminders(_ arguments: [String : AnyObject]) -> [EKAlarm]?{ let remindersArguments = arguments[remindersArgument] as? [Dictionary] if remindersArguments == nil { return nil } - + var reminders = [EKAlarm]() for reminderArguments in remindersArguments! { let minutes = reminderArguments[minutesArgument] as! Int reminders.append(EKAlarm.init(relativeOffset: 60 * Double(-minutes))) } - + return reminders } - + private func setAvailability(_ arguments: [String : AnyObject]) -> EKEventAvailability? { - guard let availabilityValue = arguments[availabilityArgument] as? String else { - return .unavailable + guard let availabilityValue = arguments[availabilityArgument] as? String else { + return .unavailable } switch availabilityValue.uppercased() { @@ -660,7 +660,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele return nil } } - + private func createOrUpdateEvent(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { checkPermissionsThenExecute(permissionsGrantedAction: { let arguments = call.arguments as! Dictionary @@ -681,12 +681,12 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele self.finishWithCalendarNotFoundError(result: result, calendarId: calendarId) return } - + if !(ekCalendar!.allowsContentModifications) { self.finishWithCalendarReadOnlyError(result: result, calendarId: calendarId) return } - + var ekEvent: EKEvent? if eventId == nil { ekEvent = EKEvent.init(eventStore: self.eventStore) @@ -697,18 +697,18 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele return } } - + ekEvent!.title = title ekEvent!.notes = description ekEvent!.isAllDay = isAllDay ekEvent!.startDate = startDate - if (isAllDay) { ekEvent!.endDate = startDate } - else { - ekEvent!.endDate = endDate - - let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current + ekEvent!.endDate = endDate + + if (!isAllDay) { + let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current ekEvent!.timeZone = timeZone - } + } + ekEvent!.calendar = ekCalendar! ekEvent!.location = location @@ -720,15 +720,15 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele else { ekEvent!.url = nil } - + ekEvent!.recurrenceRules = createEKRecurrenceRules(arguments) setAttendees(arguments, ekEvent) ekEvent!.alarms = createReminders(arguments) - + if let availability = setAvailability(arguments) { ekEvent!.availability = availability } - + do { try self.eventStore.save(ekEvent!, span: .futureEvents) result(ekEvent!.eventIdentifier) @@ -738,7 +738,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } }, result: result) } - + private func createParticipant(name: String, emailAddress: String, role: Int) -> EKParticipant? { let ekAttendeeClass: AnyClass? = NSClassFromString("EKAttendee") if let type = ekAttendeeClass as? NSObject.Type { @@ -750,7 +750,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } return nil } - + private func deleteEvent(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { checkPermissionsThenExecute(permissionsGrantedAction: { let arguments = call.arguments as! Dictionary @@ -759,25 +759,25 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let startDateNumber = arguments[eventStartDateArgument] as? NSNumber let endDateNumber = arguments[eventEndDateArgument] as? NSNumber let followingInstances = arguments[followingInstancesArgument] as? Bool - + let ekCalendar = self.eventStore.calendar(withIdentifier: calendarId) if ekCalendar == nil { self.finishWithCalendarNotFoundError(result: result, calendarId: calendarId) return } - + if !(ekCalendar!.allowsContentModifications) { self.finishWithCalendarReadOnlyError(result: result, calendarId: calendarId) return } - + if (startDateNumber == nil && endDateNumber == nil && followingInstances == nil) { let ekEvent = self.eventStore.event(withIdentifier: eventId) if ekEvent == nil { self.finishWithEventNotFoundError(result: result, eventId: eventId) return } - + do { try self.eventStore.remove(ekEvent!, span: .futureEvents) result(true) @@ -789,17 +789,17 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele else { let startDate = Date (timeIntervalSince1970: startDateNumber!.doubleValue / 1000.0) let endDate = Date (timeIntervalSince1970: endDateNumber!.doubleValue / 1000.0) - + let predicate = self.eventStore.predicateForEvents(withStart: startDate, end: endDate, calendars: nil) let foundEkEvents = self.eventStore.events(matching: predicate) as [EKEvent]? - + if foundEkEvents == nil || foundEkEvents?.count == 0 { self.finishWithEventNotFoundError(result: result, eventId: eventId) return } - + let ekEvent = foundEkEvents!.first(where: {$0.eventIdentifier == eventId}) - + do { if (!followingInstances!) { try self.eventStore.remove(ekEvent!, span: .thisEvent, commit: true) @@ -807,7 +807,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele else { try self.eventStore.remove(ekEvent!, span: .futureEvents, commit: true) } - + result(true) } catch { self.eventStore.reset() @@ -822,33 +822,33 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let arguments = call.arguments as! Dictionary let eventId = arguments[eventIdArgument] as! String let event = self.eventStore.event(withIdentifier: eventId) - + if event != nil { let eventController = EKEventViewController() eventController.event = event! eventController.delegate = self eventController.allowsEditing = true eventController.allowsCalendarPreview = true - + let flutterViewController = getTopMostViewController() let navigationController = UINavigationController(rootViewController: eventController) - + navigationController.toolbar.isTranslucent = false navigationController.toolbar.tintColor = .blue navigationController.toolbar.backgroundColor = .white flutterViewController.present(navigationController, animated: true, completion: nil) - - + + } else { result(FlutterError(code: self.genericError, message: self.eventNotFoundErrorMessageFormat, details: nil)) } }, result: result) } - + public func eventViewController(_ controller: EKEventViewController, didCompleteWith action: EKEventViewAction) { controller.dismiss(animated: true, completion: nil) - + if flutterResult != nil { switch action { case .done: @@ -862,35 +862,35 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } } } - + private func getTopMostViewController() -> UIViewController { var topController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController while ((topController?.presentedViewController) != nil) { topController = topController?.presentedViewController } - + return topController! } - + private func finishWithUnauthorizedError(result: @escaping FlutterResult) { result(FlutterError(code:self.unauthorizedErrorCode, message: self.unauthorizedErrorMessage, details: nil)) } - + private func finishWithCalendarNotFoundError(result: @escaping FlutterResult, calendarId: String) { let errorMessage = String(format: self.calendarNotFoundErrorMessageFormat, calendarId) result(FlutterError(code:self.notFoundErrorCode, message: errorMessage, details: nil)) } - + private func finishWithCalendarReadOnlyError(result: @escaping FlutterResult, calendarId: String) { let errorMessage = String(format: self.calendarReadOnlyErrorMessageFormat, calendarId) result(FlutterError(code:self.notAllowed, message: errorMessage, details: nil)) } - + private func finishWithEventNotFoundError(result: @escaping FlutterResult, eventId: String) { let errorMessage = String(format: self.eventNotFoundErrorMessageFormat, eventId) result(FlutterError(code:self.notFoundErrorCode, message: errorMessage, details: nil)) } - + private func encodeJsonAndFinish(codable: T, result: @escaping FlutterResult) { do { let jsonEncoder = JSONEncoder() @@ -901,7 +901,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele result(FlutterError(code: genericError, message: error.localizedDescription, details: nil)) } } - + private func checkPermissionsThenExecute(permissionsGrantedAction: () -> Void, result: @escaping FlutterResult) { if hasEventPermissions() { permissionsGrantedAction() @@ -909,7 +909,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } self.finishWithUnauthorizedError(result: result) } - + private func requestPermissions(completion: @escaping (Bool) -> Void) { if hasEventPermissions() { completion(true) @@ -920,12 +920,12 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele completion(accessGranted) }) } - + private func hasEventPermissions() -> Bool { let status = EKEventStore.authorizationStatus(for: .event) return status == EKAuthorizationStatus.authorized } - + private func requestPermissions(_ result: @escaping FlutterResult) { if hasEventPermissions() { result(true) @@ -964,7 +964,7 @@ extension UIColor { return nil } } - + public convenience init?(hex: String) { let r, g, b, a: CGFloat @@ -981,7 +981,7 @@ extension UIColor { r = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255 g = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255 b = CGFloat((hexNumber & 0x000000ff)) / 255 - + self.init(red: r, green: g, blue: b, alpha: a) return } @@ -991,4 +991,3 @@ extension UIColor { return nil } } - From 384890cfe36e6b34cb437daa876e10bb15646b64 Mon Sep 17 00:00:00 2001 From: "goldensoju@gmailcom" Date: Tue, 1 Nov 2022 18:28:07 +0900 Subject: [PATCH 09/43] iOS: Fix error when adding attendees due to UUID is nil. --- example/ios/Podfile.lock | 6 +++--- example/lib/presentation/pages/calendar_event.dart | 7 +++++-- example/lib/presentation/pages/event_attendee.dart | 8 ++++---- ios/Classes/SwiftDeviceCalendarPlugin.swift | 1 + test/device_calendar_test.dart | 1 - 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 09b26adb..faea259c 100755 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -25,10 +25,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: device_calendar: 9cb33f88a02e19652ec7b8b122ca778f751b1f7b - Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_native_timezone: 5f05b2de06c9776b4cc70e1839f03de178394d22 - integration_test: 7db6d89f336f671dcbc7563ee27a5b08f6f8aee1 + integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 PODFILE CHECKSUM: d3740c426905916d1f2ada0ddfce28cc99f7b7af -COCOAPODS: 1.10.1 +COCOAPODS: 1.11.3 diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index ff2a65ee..ee9040e9 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -416,8 +416,11 @@ class _CalendarEventPageState extends State { itemCount: _attendees.length, itemBuilder: (context, index) { return Container( - color: _attendees[index].isOrganiser - ? Colors.greenAccent[100] + color: (_attendees?[index].isOrganiser ?? false) + ? MediaQuery.of(context).platformBrightness == + Brightness.dark + ? Colors.black26 + : Colors.greenAccent[100] : Colors.transparent, child: ListTile( onTap: () async { diff --git a/example/lib/presentation/pages/event_attendee.dart b/example/lib/presentation/pages/event_attendee.dart index a2f17aba..d262d64f 100644 --- a/example/lib/presentation/pages/event_attendee.dart +++ b/example/lib/presentation/pages/event_attendee.dart @@ -64,7 +64,7 @@ class _EventAttendeePageState extends State { child: TextFormField( controller: _nameController, validator: (value) { - if (_attendee!.isCurrentUser == false && + if (_attendee?.isCurrentUser == false && (value == null || value.isEmpty)) { return 'Please enter a name'; } @@ -153,9 +153,9 @@ class _EventAttendeePageState extends State { name: _nameController.text, emailAddress: _emailAddressController.text, role: _role, - isOrganiser: _attendee!.isOrganiser, - isCurrentUser: _attendee!.isCurrentUser, - iosAttendeeDetails: _attendee!.iosAttendeeDetails, + isOrganiser: _attendee?.isOrganiser ?? false, + isCurrentUser: _attendee?.isCurrentUser ?? false, + iosAttendeeDetails: _attendee?.iosAttendeeDetails, androidAttendeeDetails: AndroidAttendeeDetails.fromJson( {'attendanceStatus': _status.index})); diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index 940a86b4..41863133 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -743,6 +743,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let ekAttendeeClass: AnyClass? = NSClassFromString("EKAttendee") if let type = ekAttendeeClass as? NSObject.Type { let participant = type.init() + participant.setValue(UUID().uuidString, forKey: "UUID") participant.setValue(name, forKey: "displayName") participant.setValue(emailAddress, forKey: "emailAddress") participant.setValue(role, forKey: "participantRole") diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart index 3889c44b..97c963bb 100644 --- a/test/device_calendar_test.dart +++ b/test/device_calendar_test.dart @@ -2,7 +2,6 @@ import 'package:device_calendar/device_calendar.dart'; import 'package:device_calendar/src/common/error_codes.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:timezone/timezone.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); From 2e1fa43096d108fff718eef22f43bfcf40e870e4 Mon Sep 17 00:00:00 2001 From: Julius Bredemeyer <48645716+IVLIVS-III@users.noreply.github.com> Date: Sat, 3 Dec 2022 00:27:30 +0100 Subject: [PATCH 10/43] Updated version and changelog. --- CHANGELOG.md | 5 ++++- pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bb5d1ed..6285b62b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,13 @@ -## [4.2.1](https://github.com/builttoroam/device_calendar/releases/tag/4.2.1) +## [4.3.0](https://github.com/builttoroam/device_calendar/releases/tag/4.3.0) - Updated multiple underlying dependencies - *Note:* `timezone 0.9.0` [removed named database files](https://pub.dev/packages/timezone/changelog#090). If you are only using `device_calendar`, you can ignore this note. +- Added support for all-day multi-day events on iOS +- Fixed iOS issue of adding attendees to events +- Fixed Android issue of the `ownerAccount` being null ## [4.2.0](https://github.com/builttoroam/device_calendar/releases/tag/4.2.0) diff --git a/pubspec.yaml b/pubspec.yaml index 19b57aeb..ec0732c1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,13 @@ name: device_calendar description: A cross platform plugin for modifying calendars on the user's device. -version: 4.2.1 +version: 4.3.0 homepage: https://github.com/builttoroam/device_calendar/tree/master dependencies: flutter: sdk: flutter collection: ^1.16.0 - sprintf: ^6.0.2 + sprintf: ^7.0.0 timezone: ^0.9.0 flutter_native_timezone: ^2.0.0 From 0c85ab0d158763ca0f6c93f4a8d88b5c57b87c7f Mon Sep 17 00:00:00 2001 From: Shreyas S Date: Wed, 15 Feb 2023 11:45:23 +0530 Subject: [PATCH 11/43] parseRecurrenceRuleString() when else fix. --- .../kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt index 744f636e..7141da99 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -787,6 +787,7 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { DayOfWeek.values().find { dayOfWeek -> dayOfWeek.ordinal == it.weekday.ordinal } }?.toMutableList() } + else -> recurrenceRule.daysOfWeek = null } val rfcRecurrenceRuleString = rfcRecurrenceRule.toString() From e3799b1da6679734c46bdb41218272b3a3acaf3c Mon Sep 17 00:00:00 2001 From: Julius Bredemeyer <48645716+IVLIVS-III@users.noreply.github.com> Date: Fri, 17 Feb 2023 22:16:22 +0100 Subject: [PATCH 12/43] Fixed failing test (dart analyze). --- example/lib/presentation/pages/calendar_event.dart | 2 +- lib/src/models/event.dart | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index ee9040e9..cb258367 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -416,7 +416,7 @@ class _CalendarEventPageState extends State { itemCount: _attendees.length, itemBuilder: (context, index) { return Container( - color: (_attendees?[index].isOrganiser ?? false) + color: (_attendees[index].isOrganiser) ? MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.black26 diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index c7478005..70fc9e45 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -250,6 +250,7 @@ class Event { case 'NONE': return EventStatus.None; } + return null; } bool updateStartLocation(String? newStartLocation) { From 47c851cd98ac29aeb914330fa83f86833608dc7b Mon Sep 17 00:00:00 2001 From: Julius Bredemeyer <48645716+IVLIVS-III@users.noreply.github.com> Date: Fri, 17 Feb 2023 22:16:40 +0100 Subject: [PATCH 13/43] Bumped version and updated changelog. --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6285b62b..2fa175fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ +## [4.3.1](https://github.com/builttoroam/device_calendar/releases/tag/4.3.1) + +- Fixed an [issue](https://github.com/builttoroam/device_calendar/issues/470) that prevented the plugin from being used with Kotlin 1.7.10 + ## [4.3.0](https://github.com/builttoroam/device_calendar/releases/tag/4.3.0) - Updated multiple underlying dependencies diff --git a/pubspec.yaml b/pubspec.yaml index ec0732c1..45018475 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: device_calendar description: A cross platform plugin for modifying calendars on the user's device. -version: 4.3.0 +version: 4.3.1 homepage: https://github.com/builttoroam/device_calendar/tree/master dependencies: From 472905f154e93ffb6c48c056fc8ed6db9efced60 Mon Sep 17 00:00:00 2001 From: VladyslavBilomeria Date: Sat, 13 May 2023 14:15:05 +0300 Subject: [PATCH 14/43] Fix retrieving events for more than 4 years range --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 46 ++++++++++++++++++--- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index fb6c446a..cfcfeea1 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -322,11 +322,47 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let endDate = Date (timeIntervalSince1970: endDateDateMillisecondsSinceEpoch!.doubleValue / 1000.0) let ekCalendar = self.eventStore.calendar(withIdentifier: calendarId) if ekCalendar != nil { - let predicate = self.eventStore.predicateForEvents( - withStart: startDate, - end: endDate, - calendars: [ekCalendar!]) - let ekEvents = self.eventStore.events(matching: predicate) + var ekEvents = [EKEvent]() + let fourYearsInSeconds = 4 * 365 * 24 * 60 * 60 + var currentStartDate = startDate + // Adding 4 years to the start date + var currentEndDate = startDate.addingTimeInterval(TimeInterval(fourYearsInSeconds)) + while currentEndDate <= endDate { + let rangeSize = currentEndDate.timeIntervalSince(currentStartDate) + let roundedRangeSize = Int(rangeSize / Double(fourYearsInSeconds)) * fourYearsInSeconds + + // debugPrint("Start date of current range: \(currentStartDate)") + // debugPrint("End date of current range: \(currentEndDate.addingTimeInterval(-1))") + // debugPrint("Range size: \(roundedRangeSize / (365 * 24 * 60 * 60)) years\n") + + let predicate = self.eventStore.predicateForEvents( + withStart: currentStartDate, + end: currentEndDate.addingTimeInterval(-1), + calendars: [ekCalendar!]) + let batch = self.eventStore.events(matching: predicate) + ekEvents.append(contentsOf: batch) + + // Move the start and end dates forward by the rounded range size + currentStartDate = currentEndDate + currentEndDate = currentStartDate.addingTimeInterval(TimeInterval(roundedRangeSize)) + } + + // If the cycle doesn't end exactly on the end date + if currentStartDate <= endDate { + let finalRangeSize = endDate.timeIntervalSince(currentStartDate) + + // debugPrint("Start date of final range: \(currentStartDate)") + // debugPrint("End date of final range: \(endDate)") + // debugPrint("Range size: \(finalRangeSize / (365 * 24 * 60 * 60)) years\n") + + let predicate = self.eventStore.predicateForEvents( + withStart: currentStartDate, + end: endDate, + calendars: [ekCalendar!]) + let batch = self.eventStore.events(matching: predicate) + ekEvents.append(contentsOf: batch) + } + for ekEvent in ekEvents { let event = createEventFromEkEvent(calendarId: calendarId, ekEvent: ekEvent) events.append(event) From a41b4401fd6a91135bac5c5330747307e9a188d8 Mon Sep 17 00:00:00 2001 From: VladyslavBilomeria Date: Fri, 19 May 2023 19:45:43 +0300 Subject: [PATCH 15/43] Simplify the range size calculation algorithm --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index cfcfeea1..6908345d 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -324,13 +324,11 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele if ekCalendar != nil { var ekEvents = [EKEvent]() let fourYearsInSeconds = 4 * 365 * 24 * 60 * 60 + let fourYearsTimeInterval = TimeInterval(fourYearsInSeconds) var currentStartDate = startDate // Adding 4 years to the start date - var currentEndDate = startDate.addingTimeInterval(TimeInterval(fourYearsInSeconds)) + var currentEndDate = startDate.addingTimeInterval(fourYearsTimeInterval) while currentEndDate <= endDate { - let rangeSize = currentEndDate.timeIntervalSince(currentStartDate) - let roundedRangeSize = Int(rangeSize / Double(fourYearsInSeconds)) * fourYearsInSeconds - // debugPrint("Start date of current range: \(currentStartDate)") // debugPrint("End date of current range: \(currentEndDate.addingTimeInterval(-1))") // debugPrint("Range size: \(roundedRangeSize / (365 * 24 * 60 * 60)) years\n") @@ -342,9 +340,9 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let batch = self.eventStore.events(matching: predicate) ekEvents.append(contentsOf: batch) - // Move the start and end dates forward by the rounded range size + // Move the start and end dates forward by the [fourYearsTimeInterval] currentStartDate = currentEndDate - currentEndDate = currentStartDate.addingTimeInterval(TimeInterval(roundedRangeSize)) + currentEndDate = currentStartDate.addingTimeInterval(fourYearsTimeInterval) } // If the cycle doesn't end exactly on the end date From 7304e8200f7ab820c68032ecaf24c51998e91739 Mon Sep 17 00:00:00 2001 From: VladyslavBilomeria Date: Fri, 19 May 2023 19:49:15 +0300 Subject: [PATCH 16/43] Remove logs-related code --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index 6908345d..4f56266c 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -329,10 +329,6 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele // Adding 4 years to the start date var currentEndDate = startDate.addingTimeInterval(fourYearsTimeInterval) while currentEndDate <= endDate { - // debugPrint("Start date of current range: \(currentStartDate)") - // debugPrint("End date of current range: \(currentEndDate.addingTimeInterval(-1))") - // debugPrint("Range size: \(roundedRangeSize / (365 * 24 * 60 * 60)) years\n") - let predicate = self.eventStore.predicateForEvents( withStart: currentStartDate, end: currentEndDate.addingTimeInterval(-1), @@ -347,12 +343,6 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele // If the cycle doesn't end exactly on the end date if currentStartDate <= endDate { - let finalRangeSize = endDate.timeIntervalSince(currentStartDate) - - // debugPrint("Start date of final range: \(currentStartDate)") - // debugPrint("End date of final range: \(endDate)") - // debugPrint("Range size: \(finalRangeSize / (365 * 24 * 60 * 60)) years\n") - let predicate = self.eventStore.predicateForEvents( withStart: currentStartDate, end: endDate, From 82acd9b5712ddba21d6cfe7bc94ae1930089630a Mon Sep 17 00:00:00 2001 From: Julius Bredemeyer <48645716+IVLIVS-III@users.noreply.github.com> Date: Sat, 20 May 2023 15:04:53 +0200 Subject: [PATCH 17/43] Updated example project for iOS. --- example/ios/Flutter/AppFrameworkInfo.plist | 2 +- example/ios/Podfile | 2 +- example/ios/Podfile.lock | 4 ++-- example/ios/Runner.xcodeproj/project.pbxproj | 11 +++++++---- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- example/ios/Runner/Info.plist | 4 ++++ 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 3a9c234f..9b41e7d8 100755 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 9.0 + 11.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index 8ab33cfb..997d1cb3 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '9.0' +platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index faea259c..cb8f159b 100755 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -27,8 +27,8 @@ SPEC CHECKSUMS: device_calendar: 9cb33f88a02e19652ec7b8b122ca778f751b1f7b Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_native_timezone: 5f05b2de06c9776b4cc70e1839f03de178394d22 - integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 + integration_test: 13825b8a9334a850581300559b8839134b124670 -PODFILE CHECKSUM: d3740c426905916d1f2ada0ddfce28cc99f7b7af +PODFILE CHECKSUM: 10625bdc9b9ef8574174815aabd5b048e6e29bff COCOAPODS: 1.11.3 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 7d88e2f9..160e1d14 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -161,7 +161,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1240; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -229,10 +229,12 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -243,6 +245,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -355,7 +358,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -405,7 +408,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 0254012e..14d255fd 100755 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + From 1e7ed588e2afc19d2dad069eba8a689688450598 Mon Sep 17 00:00:00 2001 From: Julius Bredemeyer <48645716+IVLIVS-III@users.noreply.github.com> Date: Sat, 20 May 2023 15:24:17 +0200 Subject: [PATCH 18/43] Fixed deprecation issues. --- test/device_calendar_test.dart | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart index ada9d769..132aad61 100644 --- a/test/device_calendar_test.dart +++ b/test/device_calendar_test.dart @@ -12,7 +12,8 @@ void main() { final log = []; setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { print('Calling channel method ${methodCall.method}'); log.add(methodCall); @@ -23,7 +24,8 @@ void main() { }); test('HasPermissions_Returns_Successfully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return true; }); @@ -34,7 +36,8 @@ void main() { }); test('RequestPermissions_Returns_Successfully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return true; }); @@ -46,7 +49,8 @@ void main() { test('RetrieveCalendars_Returns_Successfully', () async { const fakeCalendarName = 'fakeCalendarName'; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return '[{"id":"1","isReadOnly":false,"name":"$fakeCalendarName"}]'; }); @@ -114,7 +118,8 @@ void main() { test('CreateEvent_Returns_Successfully', () async { const fakeNewEventId = 'fakeNewEventId'; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return fakeNewEventId; }); @@ -133,7 +138,8 @@ void main() { test('UpdateEvent_Returns_Successfully', () async { const fakeNewEventId = 'fakeNewEventId'; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { final arguments = methodCall.arguments as Map; if (!arguments.containsKey('eventId') || arguments['eventId'] == null) { return null; From 379c99b07f1e0d7eb71a1b61525e65a177924354 Mon Sep 17 00:00:00 2001 From: "sangam.shrestha" Date: Mon, 19 Jun 2023 13:39:27 +0545 Subject: [PATCH 19/43] add proguard rules --- README.md | 4 ++++ android/build.gradle | 1 + android/proguard-rules.pro | 1 + example/android/build.gradle | 4 ++-- 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 android/proguard-rules.pro diff --git a/README.md b/README.md index a91c6e5c..893f2f89 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ If you don't need any timezone specific features in your app, you may use `flutt ```dart import 'package:flutter_native_timezone/flutter_native_timezone.dart'; +initializeTimeZones(); + // As an example, our default timezone is UTC. Location _currentLocation = getLocation('Etc/UTC'); @@ -78,6 +80,8 @@ The following will need to be added to the `AndroidManifest.xml` file for your a ``` ### Proguard / R8 exceptions +> NOTE: From v4.3.2 developers no longer need to add proguard rule in their app. + By default, all android apps go through R8 for file shrinking when building a release version. Currently, it interferes with some functions such as `retrieveCalendars()`. diff --git a/android/build.gradle b/android/build.gradle index 66d86743..c74125b7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -33,6 +33,7 @@ android { defaultConfig { minSdkVersion 16 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'proguard-rules.pro' } lintOptions { disable 'InvalidPackage' diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro new file mode 100644 index 00000000..d7668e11 --- /dev/null +++ b/android/proguard-rules.pro @@ -0,0 +1 @@ +-keep class com.builttoroam.devicecalendar.** { *; } diff --git a/example/android/build.gradle b/example/android/build.gradle index d135914c..d3f65307 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir -} \ No newline at end of file +} From 623f0bf86ab6f167f6a1eb418d42117822c942e5 Mon Sep 17 00:00:00 2001 From: MIYANARI Junki Date: Sun, 17 Sep 2023 05:49:33 +0900 Subject: [PATCH 20/43] Request FullAccess on iOS17+ (#497) * fix: request FullAccess on iOS17 or later * update iOS integration description * fix: iOS build test & Android build test --- .github/workflows/dart.yml | 5 ++++- README.md | 7 +++++++ android/build.gradle | 4 ++-- example/android/app/build.gradle | 2 +- ios/Classes/SwiftDeviceCalendarPlugin.swift | 21 ++++++++++++++++----- 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 25401783..e839df99 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -34,8 +34,11 @@ jobs: flutter build apk test-ios: name: iOS build test - runs-on: macos-latest + runs-on: macos-13 steps: + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '15.0-beta' - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: diff --git a/README.md b/README.md index 893f2f89..2af1e99d 100644 --- a/README.md +++ b/README.md @@ -111,4 +111,11 @@ For iOS 10+ support, you'll need to modify the `Info.plist` to add the following Access contacts for event attendee editing. ``` +For iOS 17+ support, add the following key/value pair as well. + +```xml +NSCalendarsFullAccessUsageDescription +Access most functions for calendar viewing and editing. +``` + Note that on iOS, this is a Swift plugin. There is a known issue being tracked [here](https://github.com/flutter/flutter/issues/16049) by the Flutter team, where adding a plugin developed in Swift to an Objective-C project causes problems. If you run into such issues, please look at the suggested workarounds there. diff --git a/android/build.gradle b/android/build.gradle index c74125b7..139fb8e5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -25,13 +25,13 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 30 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - minSdkVersion 16 + minSdkVersion 19 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'proguard-rules.pro' } diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index dd924715..776dc817 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -30,7 +30,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.builttoroam.devicecalendarexample" - minSdkVersion 16 + minSdkVersion 19 targetSdkVersion 31 versionCode 1 versionName "1.0" diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index 35d242db..f37d1a5a 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -1056,15 +1056,26 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele completion(true) return } - eventStore.requestAccess(to: .event, completion: { - (accessGranted: Bool, _: Error?) in - completion(accessGranted) - }) + if #available(iOS 17, *) { + eventStore.requestFullAccessToEvents { + (accessGranted: Bool, _: Error?) in + completion(accessGranted) + } + } else { + eventStore.requestAccess(to: .event, completion: { + (accessGranted: Bool, _: Error?) in + completion(accessGranted) + }) + } } private func hasEventPermissions() -> Bool { let status = EKEventStore.authorizationStatus(for: .event) - return status == EKAuthorizationStatus.authorized + if #available(iOS 17, *) { + return status == EKAuthorizationStatus.fullAccess + } else { + return status == EKAuthorizationStatus.authorized + } } } From 6d65268132f003f0c79a23e187210cb5d6e3e379 Mon Sep 17 00:00:00 2001 From: ashwani Date: Tue, 10 Oct 2023 17:25:51 +0100 Subject: [PATCH 21/43] included namespace --- android/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/android/build.gradle b/android/build.gradle index 139fb8e5..1f5ff500 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -46,6 +46,7 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } + namespace 'com.builttoroam.devicecalendar' } dependencies { From bddadd0cf8c8d6ca4b849a062f5b95c7f4599d55 Mon Sep 17 00:00:00 2001 From: Thomas Kam Date: Mon, 6 Nov 2023 23:21:52 -0500 Subject: [PATCH 22/43] pipeline update --- .github/workflows/dart.yml | 2 -- .github/workflows/prerelease.yml | 11 ++++------- .github/workflows/release.yml | 11 ++++------- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index e839df99..744c0ba4 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -37,8 +37,6 @@ jobs: runs-on: macos-13 steps: - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: '15.0-beta' - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index b836df6a..a08733c0 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -17,6 +17,8 @@ jobs: name: Development Release # The type of runner that the job will run on runs-on: ubuntu-latest + permissions: + id-token: write # Required for authentication using OIDC # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it @@ -36,15 +38,10 @@ jobs: run: | sed -i "0,/\#\# \[.*/s//## [${{steps.changelog_reader.outputs.version}}-$GITHUB_RUN_ID]/" CHANGELOG.md cat CHANGELOG.md - - name: Setup credentials - run: | - cat < $PUB_CACHE/credentials.json - ${{ secrets.CREDENTIALS }} - EOF - name: Publish package - run: flutter pub publish --force + uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 - name: Add entry to Github release uses: softprops/action-gh-release@v1 with: tag_name: ${{ steps.changelog_reader.outputs.version }}+${{ github.run_id }} - prerelease: true \ No newline at end of file + prerelease: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 35967b42..9d4a3728 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,8 @@ jobs: release: # The type of runner that the job will run on runs-on: ubuntu-latest + permissions: + id-token: write # Required for authentication using OIDC # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it @@ -25,13 +27,8 @@ jobs: channel: "stable" - run: dart --version - run: flutter --version - - name: Setup credentials - run: | - cat < $PUB_CACHE/credentials.json - ${{ secrets.CREDENTIALS }} - EOF - name: Publish package - run: flutter pub publish --force + uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 - name: Get Changelog Entry id: changelog_reader uses: mindsers/changelog-reader-action@v2.0.0 @@ -39,4 +36,4 @@ jobs: uses: softprops/action-gh-release@v1 with: tag_name: ${{ steps.changelog_reader.outputs.version }} - body: ${{ steps.changelog_reader.outputs.changes }} \ No newline at end of file + body: ${{ steps.changelog_reader.outputs.changes }} From d650f91256c317648b8ae8f5b0b66edab40a4e28 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Sun, 14 Jul 2024 20:57:07 +0200 Subject: [PATCH 23/43] building fixes --- example/android/app/build.gradle | 4 ++-- example/android/build.gradle | 2 +- example/lib/presentation/date_time_picker.dart | 2 +- example/lib/presentation/pages/calendars.dart | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 776dc817..7f0cf3be 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 32 + compileSdkVersion 34 ndkVersion '22.1.7171670' sourceSets { @@ -30,7 +30,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.builttoroam.devicecalendarexample" - minSdkVersion 19 + minSdkVersion flutter.minSdkVersion targetSdkVersion 31 versionCode 1 versionName "1.0" diff --git a/example/android/build.gradle b/example/android/build.gradle index d3f65307..8bbe685b 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.0' + ext.kotlin_version = '1.8.22' repositories { google() mavenCentral() diff --git a/example/lib/presentation/date_time_picker.dart b/example/lib/presentation/date_time_picker.dart index dc11e8d9..449b82e8 100644 --- a/example/lib/presentation/date_time_picker.dart +++ b/example/lib/presentation/date_time_picker.dart @@ -45,7 +45,7 @@ class DateTimePicker extends StatelessWidget { @override Widget build(BuildContext context) { - final valueStyle = Theme.of(context).textTheme.headline6; + final valueStyle = Theme.of(context).textTheme.titleLarge; return Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index 71c47ea5..389d9b02 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -46,7 +46,7 @@ class _CalendarsPageState extends State { padding: const EdgeInsets.all(10.0), child: Text( 'WARNING: some aspects of saving events are hardcoded in this example app. As such we recommend you do not modify existing events as this may result in loss of information', - style: Theme.of(context).textTheme.headline6, + style: Theme.of(context).textTheme.titleLarge, ), ), Expanded( @@ -77,7 +77,7 @@ class _CalendarsPageState extends State { Text( "${_calendars[index].id}: ${_calendars[index].name!}", style: - Theme.of(context).textTheme.subtitle1, + Theme.of(context).textTheme.titleSmall, ), Text( "Account: ${_calendars[index].accountName!}"), From 634fd1b1673f36258bd07df903a9f551665caa83 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Sun, 14 Jul 2024 20:57:44 +0200 Subject: [PATCH 24/43] added event color for android --- android/build.gradle | 2 +- .../devicecalendar/CalendarDelegate.kt | 53 +++++++++++ .../devicecalendar/DeviceCalendarPlugin.kt | 9 ++ .../devicecalendar/common/Constants.kt | 4 +- .../devicecalendar/models/Event.kt | 2 + .../presentation/pages/calendar_event.dart | 95 ++++++++++++++----- .../presentation/pages/calendar_events.dart | 7 ++ lib/device_calendar.dart | 1 + lib/src/common/channel_constants.dart | 2 + lib/src/device_calendar.dart | 16 ++++ lib/src/models/event.dart | 20 +++- lib/src/models/event_color.dart | 9 ++ 12 files changed, 194 insertions(+), 26 deletions(-) create mode 100644 lib/src/models/event_color.dart diff --git a/android/build.gradle b/android/build.gradle index 1f5ff500..34fcf1ae 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 33 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt index 1cd3f98a..888c2bff 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -39,6 +39,8 @@ import com.builttoroam.devicecalendar.common.ErrorCodes.Companion as EC import com.builttoroam.devicecalendar.common.ErrorMessages.Companion as EM import org.dmfs.rfc5545.recur.Freq as RruleFreq import org.dmfs.rfc5545.recur.RecurrenceRule as Rrule +import android.provider.CalendarContract.Colors +import androidx.collection.SparseArrayCompat private const val RETRIEVE_CALENDARS_REQUEST_CODE = 0 private const val RETRIEVE_EVENTS_REQUEST_CODE = RETRIEVE_CALENDARS_REQUEST_CODE + 1 @@ -625,6 +627,7 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : values.put(Events.DTEND, end) values.put(Events.EVENT_END_TIMEZONE, endTimeZone) values.put(Events.DURATION, duration) + values.put(Events.EVENT_COLOR_KEY, event.eventColorKey) return values } @@ -938,6 +941,7 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : val endTimeZone = cursor.getString(Cst.EVENT_PROJECTION_END_TIMEZONE_INDEX) val availability = parseAvailability(cursor.getInt(Cst.EVENT_PROJECTION_AVAILABILITY_INDEX)) val eventStatus = parseEventStatus(cursor.getInt(Cst.EVENT_PROJECTION_STATUS_INDEX)) + val eventColor = cursor.getInt(Cst.EVENT_PROJECTION_EVENT_COLOR_INDEX) val event = Event() event.eventTitle = title ?: "New Event" event.eventId = eventId.toString() @@ -953,6 +957,7 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : event.eventEndTimeZone = endTimeZone event.availability = availability event.eventStatus = eventStatus + event.eventColor = if (eventColor == 0) null else eventColor return event } @@ -1125,6 +1130,54 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : return reminders } + /** + * load available event colors for the given account name + * unable to find official documentation, so logic is based on https://android.googlesource.com/platform/packages/apps/Calendar.git/+/refs/heads/pie-release/src/com/android/calendar/EventInfoFragment.java + **/ + fun retrieveEventColors(accountName: String): List> { + val contentResolver: ContentResolver? = _context?.contentResolver + val uri: Uri = Colors.CONTENT_URI + val colors = mutableListOf() + val displayColorKeyMap = SparseArrayCompat() + + val projection = arrayOf( + Colors.COLOR, + Colors.COLOR_KEY, + ) + + // load only event colors for the given account name + val selection = "${Colors.COLOR_TYPE} = ? AND ${Colors.ACCOUNT_NAME} = ?" + val selectionArgs = arrayOf(Colors.TYPE_EVENT.toString(), accountName) + + val cursor: Cursor? = contentResolver?.query(uri, projection, selection, selectionArgs, null) + cursor?.use { + while (it.moveToNext()) { + val color = it.getInt(it.getColumnIndexOrThrow(Colors.COLOR)) + val colorKey = it.getInt(it.getColumnIndexOrThrow(Colors.COLOR_KEY)) + displayColorKeyMap.put(color, colorKey); + colors.add(color) + } + cursor.close(); + // sort colors by colorValue, since they are loaded unordered + colors.sortWith(HsvColorComparator()) + } + return colors.map { Pair(it, displayColorKeyMap[it]!! ) }.toList() + } + + /** + * Compares colors based on their hue values in the HSV color space. + * https://android.googlesource.com/platform/prebuilts/fullsdk/sources/+/refs/heads/androidx-compose-integration-release/android-34/com/android/colorpicker/HsvColorComparator.java + */ + private class HsvColorComparator : Comparator { + override fun compare(color1: Int, color2: Int): Int { + val hsv1 = FloatArray(3) + val hsv2 = FloatArray(3) + Color.colorToHSV(color1, hsv1) + Color.colorToHSV(color2, hsv2) + return hsv1[0].compareTo(hsv2[0]) + } + } + @Synchronized private fun generateUniqueRequestCodeAndCacheParameters(parameters: CalendarMethodsParametersCacheModel): Int { // TODO we can ran out of Int's at some point so this probably should re-use some of the freed ones diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt index c1f14533..2af342bd 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt @@ -25,10 +25,12 @@ private const val DELETE_EVENT_INSTANCE_METHOD = "deleteEventInstance" private const val CREATE_OR_UPDATE_EVENT_METHOD = "createOrUpdateEvent" private const val CREATE_CALENDAR_METHOD = "createCalendar" private const val DELETE_CALENDAR_METHOD = "deleteCalendar" +private const val RETRIEVE_EVENT_COLORS_METHOD = "retrieveEventColors" // Method arguments private const val CALENDAR_ID_ARGUMENT = "calendarId" private const val CALENDAR_NAME_ARGUMENT = "calendarName" +private const val CALENDAR_ACCOUNT_NAME_ARGUMENT = "accountName" private const val START_DATE_ARGUMENT = "startDate" private const val END_DATE_ARGUMENT = "endDate" private const val EVENT_IDS_ARGUMENT = "eventIds" @@ -66,6 +68,7 @@ private const val LOCAL_ACCOUNT_NAME_ARGUMENT = "localAccountName" private const val EVENT_AVAILABILITY_ARGUMENT = "availability" private const val ATTENDANCE_STATUS_ARGUMENT = "attendanceStatus" private const val EVENT_STATUS_ARGUMENT = "eventStatus" +private const val EVENT_COLOR_KEY_ARGUMENT = "eventColorKey" class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { @@ -171,6 +174,11 @@ class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { val calendarId = call.argument(CALENDAR_ID_ARGUMENT) _calendarDelegate.deleteCalendar(calendarId!!, result) } + RETRIEVE_EVENT_COLORS_METHOD -> { + val accountName = call.argument(CALENDAR_ACCOUNT_NAME_ARGUMENT) + val colors = _calendarDelegate.retrieveEventColors(accountName!!) + result.success(colors.map { listOf(it.first, it.second) }) + } else -> { result.notImplemented() } @@ -192,6 +200,7 @@ class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { event.eventURL = call.argument(EVENT_URL_ARGUMENT) event.availability = parseAvailability(call.argument(EVENT_AVAILABILITY_ARGUMENT)) event.eventStatus = parseEventStatus(call.argument(EVENT_STATUS_ARGUMENT)) + event.eventColorKey = call.argument(EVENT_COLOR_KEY_ARGUMENT) if (call.hasArgument(RECURRENCE_RULE_ARGUMENT) && call.argument>( RECURRENCE_RULE_ARGUMENT diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt index 9d136ed5..fc49f227 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt @@ -50,6 +50,7 @@ class Constants { const val EVENT_PROJECTION_END_TIMEZONE_INDEX: Int = 12 const val EVENT_PROJECTION_AVAILABILITY_INDEX: Int = 13 const val EVENT_PROJECTION_STATUS_INDEX: Int = 14 + const val EVENT_PROJECTION_EVENT_COLOR_INDEX: Int = 15 val EVENT_PROJECTION: Array = arrayOf( CalendarContract.Instances.EVENT_ID, @@ -66,7 +67,8 @@ class Constants { CalendarContract.Events.EVENT_TIMEZONE, CalendarContract.Events.EVENT_END_TIMEZONE, CalendarContract.Events.AVAILABILITY, - CalendarContract.Events.STATUS + CalendarContract.Events.STATUS, + CalendarContract.Events.EVENT_COLOR ) const val EVENT_INSTANCE_DELETION_ID_INDEX: Int = 0 diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt index 456e549f..dc988fbb 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt @@ -18,4 +18,6 @@ class Event { var reminders: MutableList = mutableListOf() var availability: Availability? = null var eventStatus: EventStatus? = null + var eventColor: Int? = null + var eventColorKey: Int? = null } \ No newline at end of file diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 72c4cb5b..47bd5a5c 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -18,14 +18,15 @@ class CalendarEventPage extends StatefulWidget { final Calendar _calendar; final Event? _event; final RecurringEventDialog? _recurringEventDialog; + final List? _eventColors; const CalendarEventPage(this._calendar, - [this._event, this._recurringEventDialog, Key? key]) + [this._event, this._recurringEventDialog, this._eventColors, Key? key]) : super(key: key); @override _CalendarEventPageState createState() { - return _CalendarEventPageState(_calendar, _event, _recurringEventDialog); + return _CalendarEventPageState(_calendar, _event, _recurringEventDialog, _eventColors); } } @@ -61,10 +62,11 @@ class _CalendarEventPageState extends State { EventStatus? _eventStatus; List? _attendees; List? _reminders; + List? _eventColors; String _timezone = 'Etc/UTC'; _CalendarEventPageState( - this._calendar, this._event, this._recurringEventDialog) { + this._calendar, this._event, this._recurringEventDialog, this._eventColors) { getCurentLocation(); } @@ -283,6 +285,30 @@ class _CalendarEventPageState extends State { }).toList(), ), ), + if (_eventColors?.isNotEmpty ?? false) + ListTile( + leading: const Text( + 'EventColor', + style: TextStyle(fontSize: 16), + ), + trailing: widget._event?.color == null ? const Text("not set") : Container( + width: 30, + height: 30, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Color(widget._event?.color ?? 0), + )), + onTap: () async { + final colors = _eventColors; + if (colors != null) { + final newColor = await selectColorDialog(colors); + if (newColor != null) { + setState(() { + _event?.updateEventColor(newColor); + }); + }} + }, + ), SwitchListTile( value: _event?.allDay ?? false, onChanged: (value) => @@ -674,11 +700,11 @@ class _CalendarEventPageState extends State { setState(() { if (value) { _rrule = _rrule?.copyWith( - byMonthDays: {1}, byWeekDays: {}); + byMonthDays: [1], byWeekDays: []); } else { _rrule = _rrule?.copyWith( - byMonthDays: {}, - byWeekDays: {ByWeekDayEntry(1, 1)}); + byMonthDays: [], + byWeekDays: [ByWeekDayEntry(1, 1)]); } }); }, @@ -694,7 +720,7 @@ class _CalendarEventPageState extends State { if (value != null) { setState(() { _rrule = _rrule - ?.copyWith(byMonths: {value.index + 1}); + ?.copyWith(byMonths: [value.index + 1]); _getValidDaysOfMonth(_rrule?.frequency); }); } @@ -722,7 +748,7 @@ class _CalendarEventPageState extends State { if (value != null) { setState(() { _rrule = - _rrule?.copyWith(byMonthDays: {value}); + _rrule?.copyWith(byMonthDays: [value]); }); } }, @@ -766,10 +792,10 @@ class _CalendarEventPageState extends State { _rrule?.byWeekDays.first.day ?? 1; setState(() { _rrule = _rrule?.copyWith( - byWeekDays: { + byWeekDays: [ ByWeekDayEntry( weekDay, value.index + 1) - }); + ]); }); } }, @@ -795,10 +821,10 @@ class _CalendarEventPageState extends State { 1; setState(() { _rrule = _rrule?.copyWith( - byWeekDays: { + byWeekDays: [ ByWeekDayEntry( value.index + 1, weekNo) - }); + ]); }); } }, @@ -825,7 +851,7 @@ class _CalendarEventPageState extends State { if (value != null) { setState(() { _rrule = _rrule?.copyWith( - byMonths: {value.index + 1}); + byMonths: [value.index + 1]); }); } }, @@ -1068,22 +1094,22 @@ class _CalendarEventPageState extends State { void _updateDaysOfWeek() { switch (_dayOfWeekGroup) { case DayOfWeekGroup.Weekday: - _rrule = _rrule?.copyWith(byWeekDays: { + _rrule = _rrule?.copyWith(byWeekDays: [ ByWeekDayEntry(1), ByWeekDayEntry(2), ByWeekDayEntry(3), ByWeekDayEntry(4), ByWeekDayEntry(5), - }); + ]); break; case DayOfWeekGroup.Weekend: - _rrule = _rrule?.copyWith(byWeekDays: { + _rrule = _rrule?.copyWith(byWeekDays: [ ByWeekDayEntry(6), ByWeekDayEntry(7), - }); + ]); break; case DayOfWeekGroup.AllDays: - _rrule = _rrule?.copyWith(byWeekDays: { + _rrule = _rrule?.copyWith(byWeekDays: [ ByWeekDayEntry(1), ByWeekDayEntry(2), ByWeekDayEntry(3), @@ -1091,7 +1117,7 @@ class _CalendarEventPageState extends State { ByWeekDayEntry(5), ByWeekDayEntry(6), ByWeekDayEntry(7), - }); + ]); break; case DayOfWeekGroup.None: default: @@ -1138,7 +1164,7 @@ class _CalendarEventPageState extends State { } } - int _weekNumFromWeekDayOccurence(Set weekdays) { + int _weekNumFromWeekDayOccurence(List weekdays) { final weekNum = weekdays.first.occurrence; if (weekNum != null) { return weekNum - 1; @@ -1168,7 +1194,7 @@ class _CalendarEventPageState extends State { } if (!hasByWeekDays && !hasByMonthDays) { _rrule = rrule - .copyWith(frequency: freq, byWeekDays: {ByWeekDayEntry(1, 1)}); + .copyWith(frequency: freq, byWeekDays: [ByWeekDayEntry(1, 1)]); } else { _rrule = rrule.copyWith(frequency: freq); } @@ -1177,8 +1203,8 @@ class _CalendarEventPageState extends State { if (!hasByWeekDays || !hasByMonths) { _rrule = rrule.copyWith( frequency: freq, - byWeekDays: {ByWeekDayEntry(1, 1)}, - byMonths: {1}); + byWeekDays: [ByWeekDayEntry(1, 1)], + byMonths: [1]); } else { _rrule = rrule.copyWith(frequency: freq); } @@ -1258,4 +1284,27 @@ class _CalendarEventPageState extends State { void showInSnackBar(BuildContext context, String value) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(value))); } + + Future selectColorDialog(List colors) async { + return await showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: const Text('Select Event color'), + children: colors.map((color) => + SimpleDialogOption( + onPressed: () { Navigator.pop(context, color); }, + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Color(color.color)), + ), + ) + ).toList() + ); + } + ); + } } diff --git a/example/lib/presentation/pages/calendar_events.dart b/example/lib/presentation/pages/calendar_events.dart index a8d4b2b2..781b39b4 100644 --- a/example/lib/presentation/pages/calendar_events.dart +++ b/example/lib/presentation/pages/calendar_events.dart @@ -24,6 +24,7 @@ class _CalendarEventsPageState extends State { late DeviceCalendarPlugin _deviceCalendarPlugin; List _calendarEvents = []; + List? _eventColors; bool _isLoading = true; _CalendarEventsPageState(this._calendar) { @@ -33,6 +34,7 @@ class _CalendarEventsPageState extends State { @override void initState() { super.initState(); + _retrieveEventColors(); _retrieveCalendarEvents(); } @@ -123,6 +125,7 @@ class _CalendarEventsPageState extends State { _onLoading, _onDeletedFinished, ), + _eventColors ); })); if (refreshEvents != null && refreshEvents) { @@ -142,6 +145,10 @@ class _CalendarEventsPageState extends State { }); } + void _retrieveEventColors() async { + _eventColors = await _deviceCalendarPlugin.retrieveEventColors(_calendar); + } + Widget _getDeleteButton() { return IconButton( icon: const Icon(Icons.delete), diff --git a/lib/device_calendar.dart b/lib/device_calendar.dart index 3566d5df..b3a7c906 100644 --- a/lib/device_calendar.dart +++ b/lib/device_calendar.dart @@ -6,6 +6,7 @@ export 'src/models/calendar.dart'; export 'src/models/result.dart'; export 'src/models/reminder.dart'; export 'src/models/event.dart'; +export 'src/models/event_color.dart'; export 'src/models/retrieve_events_params.dart'; export 'package:rrule/rrule.dart'; export 'package:rrule/src/frequency.dart'; diff --git a/lib/src/common/channel_constants.dart b/lib/src/common/channel_constants.dart index 2eef3d2d..4c1890d5 100644 --- a/lib/src/common/channel_constants.dart +++ b/lib/src/common/channel_constants.dart @@ -11,6 +11,7 @@ class ChannelConstants { static const String methodNameCreateCalendar = 'createCalendar'; static const String methodNameDeleteCalendar = 'deleteCalendar'; static const String methodNameShowiOSEventModal = 'showiOSEventModal'; + static const String methodNameRetrieveEventColors = 'retrieveEventColors'; static const String parameterNameCalendarId = 'calendarId'; static const String parameterNameStartDate = 'startDate'; @@ -23,4 +24,5 @@ class ChannelConstants { static const String parameterNameCalendarName = 'calendarName'; static const String parameterNameCalendarColor = 'calendarColor'; static const String parameterNameLocalAccountName = 'localAccountName'; + static const String parameterAccountName = "accountName"; } diff --git a/lib/src/device_calendar.dart b/lib/src/device_calendar.dart index 4c1d12f1..b48d8971 100644 --- a/lib/src/device_calendar.dart +++ b/lib/src/device_calendar.dart @@ -2,6 +2,7 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:io'; +import 'package:device_calendar/src/models/event_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:timezone/data/latest.dart' as tz; @@ -342,6 +343,21 @@ class DeviceCalendarPlugin { ); } + Future?> retrieveEventColors(Calendar calendar) async { + if (!Platform.isAndroid) { + return null; + } + final accountName = calendar.accountName; + if (accountName == null) { + return []; + } + final dynamic colors = await _invokeChannelMethod(ChannelConstants.methodNameRetrieveEventColors, + arguments: () => { + ChannelConstants.parameterAccountName: accountName, + },); + return (colors.data as List).cast().map((color) => EventColor(color[0], color[1])).toList(); + } + Future> _invokeChannelMethod( String channelMethodName, { Function(Result)? assertParameters, diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index 94ef6217..12e359d8 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; +import 'event_color.dart'; import '../../device_calendar.dart'; import '../common/error_messages.dart'; @@ -49,6 +50,15 @@ class Event { /// Indicates if this event is of confirmed, canceled, tentative or none status EventStatus? status; + /// Read-only. Color of the event + int? get color=> _color; + + /// The color of this event + int? _color; + + /// The color key of this event. This is needed to change the event color + int? _colorKey; + ///Note for development: /// ///JSON field names are coded in dart, swift and kotlin to facilitate data exchange. @@ -110,6 +120,7 @@ class Event { calendarId = json['calendarId']; title = json['eventTitle']; description = json['eventDescription']; + _color = json['eventColor']; startTimestamp = json['eventStartDate']; startLocationName = json['eventStartTimeZone']; @@ -237,6 +248,8 @@ class Event { data['eventURL'] = url?.data?.contentText; data['availability'] = availability.enumToString; data['eventStatus'] = status?.enumToString; + data['eventColor'] = color; + data['eventColorKey'] = _colorKey; if (attendees != null) { data['attendees'] = attendees?.map((a) => a?.toJson()).toList(); @@ -310,4 +323,9 @@ class Event { return false; } } -} + + void updateEventColor(EventColor eventColor) { + _color = eventColor.color; + _colorKey = eventColor.colorKey; + } +} \ No newline at end of file diff --git a/lib/src/models/event_color.dart b/lib/src/models/event_color.dart new file mode 100644 index 00000000..0851bf23 --- /dev/null +++ b/lib/src/models/event_color.dart @@ -0,0 +1,9 @@ +class EventColor { + final int color; + final int colorKey; + + EventColor(this.color, this.colorKey); + + @override + String toString() => 'EventColor(color: $color, colorKey: $colorKey)'; +} \ No newline at end of file From 7dce8d1a88eedfccd4f927dab3486596170bc894 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Sun, 14 Jul 2024 21:08:54 +0200 Subject: [PATCH 25/43] added removing of event color --- .../presentation/pages/calendar_event.dart | 22 +++++++++++++------ lib/src/models/event.dart | 6 ++--- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 47bd5a5c..82af5695 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -302,11 +302,10 @@ class _CalendarEventPageState extends State { final colors = _eventColors; if (colors != null) { final newColor = await selectColorDialog(colors); - if (newColor != null) { - setState(() { - _event?.updateEventColor(newColor); - }); - }} + setState(() { + _event?.updateEventColor(newColor); + }); + } }, ), SwitchListTile( @@ -1288,10 +1287,19 @@ class _CalendarEventPageState extends State { Future selectColorDialog(List colors) async { return await showDialog( context: context, + barrierDismissible: false, builder: (BuildContext context) { return SimpleDialog( title: const Text('Select Event color'), - children: colors.map((color) => + children: [ + SimpleDialogOption( + onPressed: () { Navigator.pop(context, null); }, + child: const Padding( + padding: EdgeInsets.all(16.0), + child: Text('Reset', textAlign: TextAlign.center,), + ), + ), + ...colors.map((color) => SimpleDialogOption( onPressed: () { Navigator.pop(context, color); }, child: Container( @@ -1302,7 +1310,7 @@ class _CalendarEventPageState extends State { color: Color(color.color)), ), ) - ).toList() + )] ); } ); diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index 12e359d8..2c20e48f 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -324,8 +324,8 @@ class Event { } } - void updateEventColor(EventColor eventColor) { - _color = eventColor.color; - _colorKey = eventColor.colorKey; + void updateEventColor(EventColor? eventColor) { + _color = eventColor?.color; + _colorKey = eventColor?.colorKey; } } \ No newline at end of file From a669955e3792581aa94322d4d3bf71a872684069 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Mon, 22 Jul 2024 23:39:15 +0200 Subject: [PATCH 26/43] added updating of calendar color for android and ios --- .../devicecalendar/CalendarDelegate.kt | 23 ++++- .../devicecalendar/DeviceCalendarPlugin.kt | 24 ++++- .../presentation/pages/calendar_event.dart | 43 ++------- example/lib/presentation/pages/calendars.dart | 87 +++++++++++++++---- .../pages/color_picker_dialog.dart | 28 ++++++ ios/Classes/SwiftDeviceCalendarPlugin.swift | 30 +++++++ lib/device_calendar.dart | 1 + lib/src/common/channel_constants.dart | 3 + lib/src/device_calendar.dart | 76 +++++++++++++--- 9 files changed, 245 insertions(+), 70 deletions(-) create mode 100644 example/lib/presentation/pages/color_picker_dialog.dart diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt index 888c2bff..165c720a 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -1134,7 +1134,7 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : * load available event colors for the given account name * unable to find official documentation, so logic is based on https://android.googlesource.com/platform/packages/apps/Calendar.git/+/refs/heads/pie-release/src/com/android/calendar/EventInfoFragment.java **/ - fun retrieveEventColors(accountName: String): List> { + private fun retrieveColors(accountName: String, colorType: Int): List> { val contentResolver: ContentResolver? = _context?.contentResolver val uri: Uri = Colors.CONTENT_URI val colors = mutableListOf() @@ -1147,7 +1147,8 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : // load only event colors for the given account name val selection = "${Colors.COLOR_TYPE} = ? AND ${Colors.ACCOUNT_NAME} = ?" - val selectionArgs = arrayOf(Colors.TYPE_EVENT.toString(), accountName) + val selectionArgs = arrayOf(colorType.toString(), accountName) + val cursor: Cursor? = contentResolver?.query(uri, projection, selection, selectionArgs, null) cursor?.use { @@ -1164,6 +1165,24 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : return colors.map { Pair(it, displayColorKeyMap[it]!! ) }.toList() } + fun retrieveEventColors(accountName: String): List> { + return retrieveColors(accountName, Colors.TYPE_EVENT) + } + fun retrieveCalendarColors(accountName: String): List> { + return retrieveColors(accountName, Colors.TYPE_CALENDAR) + } + + fun updateCalendarColor(calendarId: Long, newColorKey: Int?, newColor: Int?): Boolean { + val contentResolver: ContentResolver? = _context?.contentResolver + val uri: Uri = ContentUris.withAppendedId(CalendarContract.Calendars.CONTENT_URI, calendarId) + val values = ContentValues().apply { + put(CalendarContract.Calendars.CALENDAR_COLOR_KEY, newColorKey) + put(CalendarContract.Calendars.CALENDAR_COLOR, newColor) + } + val rows = contentResolver?.update(uri, values, null, null) + return (rows ?: 0) > 0 + } + /** * Compares colors based on their hue values in the HSV color space. * https://android.googlesource.com/platform/prebuilts/fullsdk/sources/+/refs/heads/androidx-compose-integration-release/android-34/com/android/colorpicker/HsvColorComparator.java diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt index 2af342bd..bde9ce5a 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt @@ -26,6 +26,8 @@ private const val CREATE_OR_UPDATE_EVENT_METHOD = "createOrUpdateEvent" private const val CREATE_CALENDAR_METHOD = "createCalendar" private const val DELETE_CALENDAR_METHOD = "deleteCalendar" private const val RETRIEVE_EVENT_COLORS_METHOD = "retrieveEventColors" +private const val RETRIEVE_CALENDAR_COLORS_METHOD = "retrieveCalendarColors" +private const val UPDATE_CALENDAR_COLOR = "updateCalendarColor" // Method arguments private const val CALENDAR_ID_ARGUMENT = "calendarId" @@ -69,6 +71,7 @@ private const val EVENT_AVAILABILITY_ARGUMENT = "availability" private const val ATTENDANCE_STATUS_ARGUMENT = "attendanceStatus" private const val EVENT_STATUS_ARGUMENT = "eventStatus" private const val EVENT_COLOR_KEY_ARGUMENT = "eventColorKey" +private const val CALENDAR_COLOR_KEY_ARGUMENT = "calendarColorKey" class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { @@ -176,9 +179,28 @@ class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } RETRIEVE_EVENT_COLORS_METHOD -> { val accountName = call.argument(CALENDAR_ACCOUNT_NAME_ARGUMENT) - val colors = _calendarDelegate.retrieveEventColors(accountName!!) + val colors = _calendarDelegate.retrieveEventColors(accountName!!, ) result.success(colors.map { listOf(it.first, it.second) }) } + RETRIEVE_CALENDAR_COLORS_METHOD -> { + val accountName = call.argument(CALENDAR_ACCOUNT_NAME_ARGUMENT) + if (accountName == null) { + return []; + } + val colors = _calendarDelegate.retrieveCalendarColors(accountName) + result.success(colors.map { listOf(it.first, it.second) }) + } + UPDATE_CALENDAR_COLOR -> { + val calendarId = call.argument(CALENDAR_ID_ARGUMENT)?.toLong() + if (calendarId == null) { + result.success(false) + return + } + val newColorKey = (call.argument(CALENDAR_COLOR_KEY_ARGUMENT))?.toInt() + val newColor = (call.argument(CALENDAR_COLOR_ARGUMENT))?.toInt() + val success = _calendarDelegate.updateCalendarColor(calendarId, newColorKey, newColor) + result.success(success) + } else -> { result.notImplemented() } diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 82af5695..523024e7 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:device_calendar/device_calendar.dart'; +import 'package:device_calendar_example/presentation/pages/color_picker_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_native_timezone/flutter_native_timezone.dart'; @@ -299,13 +300,13 @@ class _CalendarEventPageState extends State { color: Color(widget._event?.color ?? 0), )), onTap: () async { - final colors = _eventColors; - if (colors != null) { - final newColor = await selectColorDialog(colors); + if (_eventColors != null) { + final colors = _eventColors?.map((eventColor) => Color(eventColor.color)).toList(); + final newColor = await ColorPickerDialog.selectColorDialog(colors ?? [], context); setState(() { - _event?.updateEventColor(newColor); + _event?.updateEventColor(_eventColors?.firstWhereOrNull((eventColor) => eventColor.color == newColor?.value)); }); - } + } }, ), SwitchListTile( @@ -1283,36 +1284,4 @@ class _CalendarEventPageState extends State { void showInSnackBar(BuildContext context, String value) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(value))); } - - Future selectColorDialog(List colors) async { - return await showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return SimpleDialog( - title: const Text('Select Event color'), - children: [ - SimpleDialogOption( - onPressed: () { Navigator.pop(context, null); }, - child: const Padding( - padding: EdgeInsets.all(16.0), - child: Text('Reset', textAlign: TextAlign.center,), - ), - ), - ...colors.map((color) => - SimpleDialogOption( - onPressed: () { Navigator.pop(context, color); }, - child: Container( - width: 48, - height: 48, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Color(color.color)), - ), - ) - )] - ); - } - ); - } } diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index 389d9b02..3c8bf1f5 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -1,7 +1,11 @@ +import 'dart:io'; + import 'package:device_calendar/device_calendar.dart'; import 'package:device_calendar_example/presentation/pages/calendar_add.dart'; +import 'package:device_calendar_example/presentation/pages/color_picker_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:collection/collection.dart'; import 'calendar_events.dart'; @@ -17,6 +21,7 @@ class CalendarsPage extends StatefulWidget { class _CalendarsPageState extends State { late DeviceCalendarPlugin _deviceCalendarPlugin; List _calendars = []; + List get _writableCalendars => _calendars.where((c) => c.isReadOnly == false).toList(); @@ -46,7 +51,10 @@ class _CalendarsPageState extends State { padding: const EdgeInsets.all(10.0), child: Text( 'WARNING: some aspects of saving events are hardcoded in this example app. As such we recommend you do not modify existing events as this may result in loss of information', - style: Theme.of(context).textTheme.titleLarge, + style: Theme + .of(context) + .textTheme + .titleLarge, ), ), Expanded( @@ -55,15 +63,13 @@ class _CalendarsPageState extends State { itemCount: _calendars.length, itemBuilder: (BuildContext context, int index) { return GestureDetector( - key: Key(_calendars[index].isReadOnly == true - ? 'readOnlyCalendar${_readOnlyCalendars.indexWhere((c) => c.id == _calendars[index].id)}' - : 'writableCalendar${_writableCalendars.indexWhere((c) => c.id == _calendars[index].id)}'), + key: ValueKey(_calendars[index].color), onTap: () async { await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { - return CalendarEventsPage(_calendars[index], - key: const Key('calendarEventsPage')); - })); + return CalendarEventsPage(_calendars[index], + key: const Key('calendarEventsPage')); + })); }, child: Padding( padding: const EdgeInsets.all(10.0), @@ -75,21 +81,64 @@ class _CalendarsPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "${_calendars[index].id}: ${_calendars[index].name!}", + "${_calendars[index] + .id}: ${_calendars[index].name!}", style: - Theme.of(context).textTheme.titleSmall, + Theme + .of(context) + .textTheme + .titleSmall, ), Text( - "Account: ${_calendars[index].accountName!}"), + "Account: ${_calendars[index] + .accountName!}"), Text( "type: ${_calendars[index].accountType}"), ])), - Container( - width: 15, - height: 15, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Color(_calendars[index].color!)), + GestureDetector( + onTap: () async { + final calendar = _calendars[index]; + final googleCalendarColors = await _deviceCalendarPlugin + .retrieveCalendarColors(_calendars[index]); + final colors = googleCalendarColors.isNotEmpty + ? googleCalendarColors.map((calendarColor) => + Color(calendarColor.color)).toList() + : [ + Colors.red, + Colors.green, + Colors.blue, + Colors.yellow, + Colors.orange, + Colors.purple, + Colors.cyan, + Colors.pink, + Colors.brown, + Colors.grey, + ]; + final color = await ColorPickerDialog + .selectColorDialog(colors, context); + if (color != null) { + final success = await _deviceCalendarPlugin + .updateCalendarColor(calendar, + calendarColor: googleCalendarColors + .firstWhereOrNull((calendarColor) => + calendarColor.color == color.value), + color: color); + if (success) { + _retrieveCalendars(); + } + } + }, + child: Container( + key: ValueKey(_calendars[index].color), + margin: const EdgeInsets.symmetric( + horizontal: 5, vertical: 10), + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Color(_calendars[index].color!)), + ), ), const SizedBox(width: 10), if (_calendars[index].isDefault!) @@ -116,8 +165,8 @@ class _CalendarsPageState extends State { onPressed: () async { final createCalendar = await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { - return const CalendarAddPage(); - })); + return const CalendarAddPage(); + })); if (createCalendar == true) { _retrieveCalendars(); @@ -158,4 +207,4 @@ class _CalendarsPageState extends State { _retrieveCalendars(); }); } -} +} \ No newline at end of file diff --git a/example/lib/presentation/pages/color_picker_dialog.dart b/example/lib/presentation/pages/color_picker_dialog.dart new file mode 100644 index 00000000..04d7fc7d --- /dev/null +++ b/example/lib/presentation/pages/color_picker_dialog.dart @@ -0,0 +1,28 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class ColorPickerDialog { + static Future selectColorDialog(List colors, BuildContext context) async { + return await showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: const Text('Select color'), + children: [ + ...colors.map((color) => + SimpleDialogOption( + onPressed: () { Navigator.pop(context, color); }, + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color), + ), + ) + )] + ); + } + ); + } +} \ No newline at end of file diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index f37d1a5a..f1d519a7 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -114,6 +114,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let deleteEventMethod = "deleteEvent" let deleteEventInstanceMethod = "deleteEventInstance" let showEventModalMethod = "showiOSEventModal" + let updateCalendarColor = "updateCalendarColor" let calendarIdArgument = "calendarId" let startDateArgument = "startDate" let endDateArgument = "endDate" @@ -185,6 +186,8 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele case showEventModalMethod: self.flutterResult = result showEventModal(call, result) + case updateCalendarColor: + updateCalendarColor(call, result) default: result(FlutterMethodNotImplemented) } @@ -245,6 +248,33 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } } + + + private func createCalendar(_ call: FlutterMethodCall, _ result: FlutterResult) { + let arguments = call.arguments as! Dictionary + let arguments = call.arguments as! Dictionary + let calendarId = arguments[calendarIdArgument] as! String + let color = arguments[calendarColorArgument] as! Int + + guard let calendar = eventStore.calendar(withIdentifier: calendarIdentifier) else { + print("Calendar not found") + result(false) + return + } + + // Update the calendar color + calendar.cgColor = UIColorFromRGB(color ?? 0)?.cgColor + + // Save the changes + do { + try eventStore.saveCalendar(calendar, commit: true) + result(false) + } catch { + result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) + } + } + } + private func retrieveCalendars(_ result: @escaping FlutterResult) { checkPermissionsThenExecute(permissionsGrantedAction: { let ekCalendars = self.eventStore.calendars(for: .event) diff --git a/lib/device_calendar.dart b/lib/device_calendar.dart index b3a7c906..ab9f78d6 100644 --- a/lib/device_calendar.dart +++ b/lib/device_calendar.dart @@ -7,6 +7,7 @@ export 'src/models/result.dart'; export 'src/models/reminder.dart'; export 'src/models/event.dart'; export 'src/models/event_color.dart'; +export 'src/models/calendar_color.dart'; export 'src/models/retrieve_events_params.dart'; export 'package:rrule/rrule.dart'; export 'package:rrule/src/frequency.dart'; diff --git a/lib/src/common/channel_constants.dart b/lib/src/common/channel_constants.dart index 4c1890d5..b56f8adf 100644 --- a/lib/src/common/channel_constants.dart +++ b/lib/src/common/channel_constants.dart @@ -12,6 +12,8 @@ class ChannelConstants { static const String methodNameDeleteCalendar = 'deleteCalendar'; static const String methodNameShowiOSEventModal = 'showiOSEventModal'; static const String methodNameRetrieveEventColors = 'retrieveEventColors'; + static const String methodNameRetrieveCalendarColors = 'retrieveCalendarColors'; + static const String methodNameUpdateCalendarColor = 'updateCalendarColor'; static const String parameterNameCalendarId = 'calendarId'; static const String parameterNameStartDate = 'startDate'; @@ -23,6 +25,7 @@ class ChannelConstants { static const String parameterNameFollowingInstances = 'followingInstances'; static const String parameterNameCalendarName = 'calendarName'; static const String parameterNameCalendarColor = 'calendarColor'; + static const String parameterNameCalendarColorKey = 'calendarColorKey'; static const String parameterNameLocalAccountName = 'localAccountName'; static const String parameterAccountName = "accountName"; } diff --git a/lib/src/device_calendar.dart b/lib/src/device_calendar.dart index b48d8971..6fc778e9 100644 --- a/lib/src/device_calendar.dart +++ b/lib/src/device_calendar.dart @@ -2,19 +2,14 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:io'; -import 'package:device_calendar/src/models/event_color.dart'; +import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:timezone/data/latest.dart' as tz; -import 'package:timezone/timezone.dart'; import 'common/channel_constants.dart'; import 'common/error_codes.dart'; import 'common/error_messages.dart'; -import 'models/calendar.dart'; -import 'models/event.dart'; -import 'models/result.dart'; -import 'models/retrieve_events_params.dart'; /// Provides functionality for working with device calendar(s) class DeviceCalendarPlugin { @@ -343,7 +338,7 @@ class DeviceCalendarPlugin { ); } - Future?> retrieveEventColors(Calendar calendar) async { + Future?> retrieveEventColors(Calendar calendar) async { if (!Platform.isAndroid) { return null; } @@ -351,11 +346,70 @@ class DeviceCalendarPlugin { if (accountName == null) { return []; } - final dynamic colors = await _invokeChannelMethod(ChannelConstants.methodNameRetrieveEventColors, + final dynamic colors = await _invokeChannelMethod( + ChannelConstants.methodNameRetrieveEventColors, arguments: () => { - ChannelConstants.parameterAccountName: accountName, - },); - return (colors.data as List).cast().map((color) => EventColor(color[0], color[1])).toList(); + ChannelConstants.parameterAccountName: accountName, + }, + ); + return (colors.data as List) + .cast() + .map((color) => EventColor(color[0], color[1])) + .toList(); + } + + /// Retrieves available colors for Google Calendars. + /// + /// For non-Google calendars, an empty list is returned. Use the `color` parameter in [updateCalendarColor] for these. + /// + /// [calendar] The calendar to retrieve colors for. + /// + /// Returns a List with available colors for Google Calendars or an empty list for others. + Future> retrieveCalendarColors(Calendar calendar) async { + if (!Platform.isAndroid) { + return []; + } + final accountName = calendar.accountName; + if (accountName == null) { + return []; + } + final dynamic colors = await _invokeChannelMethod( + ChannelConstants.methodNameRetrieveCalendarColors, + arguments: () => { + ChannelConstants.parameterAccountName: accountName, + }, + ); + return (colors.data as List) + .cast() + .map((color) => CalendarColor(color[0], color[1])) + .toList(); + } + + /// Updates the color of a calendar using Google Calendar colors or platform-specific colors. + /// [calendar] The calendar to update. Must have a non-null `id`. + /// [calendarColor] Required for Google Calendars where [retrieveCalendarColors] is not empty. + /// [color] Required for locale or iOS Calendars where [retrieveCalendarColors] is empty. + /// + /// Returns `true` if the update was successful, otherwise `false`. + Future updateCalendarColor(Calendar calendar, + {CalendarColor? calendarColor, Color? color}) async { + final calendarId = calendar.id; + if (calendarId == null || color == null && calendarColor == null) { + return false; + } + final result = await _invokeChannelMethod( + ChannelConstants.methodNameUpdateCalendarColor, + arguments: () => { + ChannelConstants.parameterNameCalendarId: Platform.isAndroid ? int.tryParse(calendarId) : calendarId, + ChannelConstants.parameterNameCalendarColorKey: calendarColor?.colorKey, + ChannelConstants.parameterNameCalendarColor: color?.value, + }, + ); + final success = (result.data as bool?) ?? false; + if (success) { + calendar.color = color?.value ?? calendarColor?.color; + } + return success; } Future> _invokeChannelMethod( From f44704179a52358cee7fd315e602a0e477d76e19 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Tue, 23 Jul 2024 00:10:02 +0200 Subject: [PATCH 27/43] fixes for iOS updateCalendarcolor --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 25 ++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index f1d519a7..323c8f5a 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -248,31 +248,36 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } } - - - private func createCalendar(_ call: FlutterMethodCall, _ result: FlutterResult) { - let arguments = call.arguments as! Dictionary + private func updateCalendarColor(_ call: FlutterMethodCall, _ result: FlutterResult) { let arguments = call.arguments as! Dictionary let calendarId = arguments[calendarIdArgument] as! String let color = arguments[calendarColorArgument] as! Int - - guard let calendar = eventStore.calendar(withIdentifier: calendarIdentifier) else { + + guard let calendar = eventStore.calendar(withIdentifier: calendarId) else { print("Calendar not found") result(false) return } - + // Update the calendar color - calendar.cgColor = UIColorFromRGB(color ?? 0)?.cgColor - + calendar.cgColor = UIColorFromRGB(color).cgColor + // Save the changes do { try eventStore.saveCalendar(calendar, commit: true) - result(false) + result(true) // Assuming the operation was successful, return true } catch { result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) } } + + func UIColorFromRGB(_ rgbValue: Int) -> UIColor { + return UIColor( + red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, + green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0, + blue: CGFloat(rgbValue & 0x0000FF) / 255.0, + alpha: CGFloat(1.0) + ) } private func retrieveCalendars(_ result: @escaping FlutterResult) { From c333f314b77353b923e45628d42272dd4db6c7df Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Tue, 23 Jul 2024 14:38:53 +0200 Subject: [PATCH 28/43] small fix --- .../com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt index bde9ce5a..a5d7df80 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt @@ -179,13 +179,18 @@ class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } RETRIEVE_EVENT_COLORS_METHOD -> { val accountName = call.argument(CALENDAR_ACCOUNT_NAME_ARGUMENT) + if (accountName == null) { + result.success(intArrayOf()) + return; + } val colors = _calendarDelegate.retrieveEventColors(accountName!!, ) result.success(colors.map { listOf(it.first, it.second) }) } RETRIEVE_CALENDAR_COLORS_METHOD -> { val accountName = call.argument(CALENDAR_ACCOUNT_NAME_ARGUMENT) if (accountName == null) { - return []; + result.success(intArrayOf()) + return; } val colors = _calendarDelegate.retrieveCalendarColors(accountName) result.success(colors.map { listOf(it.first, it.second) }) From cd0c652380fcc5767ca5c5acdfe371479ed3b464 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Tue, 23 Jul 2024 21:56:10 +0200 Subject: [PATCH 29/43] added calendar color file --- lib/src/models/calendar_color.dart | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 lib/src/models/calendar_color.dart diff --git a/lib/src/models/calendar_color.dart b/lib/src/models/calendar_color.dart new file mode 100644 index 00000000..e63e0be1 --- /dev/null +++ b/lib/src/models/calendar_color.dart @@ -0,0 +1,9 @@ +class CalendarColor { + final int color; + final int colorKey; + + CalendarColor(this.color, this.colorKey); + + @override + String toString() => 'CalendarColor(color: $color, colorKey: $colorKey)'; +} \ No newline at end of file From 75ee949f416aaaa9fafdfd976b66d069a8828311 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Mon, 12 Aug 2024 12:09:10 +0200 Subject: [PATCH 30/43] made colorKey accessible in event --- lib/src/models/event.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index 2c20e48f..1c5fe91f 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -53,10 +53,13 @@ class Event { /// Read-only. Color of the event int? get color=> _color; - /// The color of this event + /// Read-only. Color of the event + int? get colorKey=> _colorKey; + + /// Only updatable for Android calendars where [DeviceCalendarPlugin.retrieveEventColors] returns an empty list. int? _color; - /// The color key of this event. This is needed to change the event color + /// Only updatable for colors of [DeviceCalendarPlugin.retrieveEventColors]. int? _colorKey; ///Note for development: From 72b58f27bf0af6975166381efa467ef8a50ad412 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Mon, 12 Aug 2024 14:20:55 +0200 Subject: [PATCH 31/43] set eventcolor, loaded eventColorKey for event --- .../kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt | 3 +++ .../kotlin/com/builttoroam/devicecalendar/common/Constants.kt | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt index 165c720a..dd73716f 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -628,6 +628,7 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : values.put(Events.EVENT_END_TIMEZONE, endTimeZone) values.put(Events.DURATION, duration) values.put(Events.EVENT_COLOR_KEY, event.eventColorKey) + values.put(Events.EVENT_COLOR, event.eventColor) return values } @@ -942,6 +943,7 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : val availability = parseAvailability(cursor.getInt(Cst.EVENT_PROJECTION_AVAILABILITY_INDEX)) val eventStatus = parseEventStatus(cursor.getInt(Cst.EVENT_PROJECTION_STATUS_INDEX)) val eventColor = cursor.getInt(Cst.EVENT_PROJECTION_EVENT_COLOR_INDEX) + val eventColorKey = cursor.getInt(Cst.EVENT_PROJECTION_EVENT_COLOR_KEY_INDEX) val event = Event() event.eventTitle = title ?: "New Event" event.eventId = eventId.toString() @@ -958,6 +960,7 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : event.availability = availability event.eventStatus = eventStatus event.eventColor = if (eventColor == 0) null else eventColor + event.eventColorKey = if (eventColorKey == 0) null else eventColorKey return event } diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt index fc49f227..f02eebd2 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt @@ -51,6 +51,7 @@ class Constants { const val EVENT_PROJECTION_AVAILABILITY_INDEX: Int = 13 const val EVENT_PROJECTION_STATUS_INDEX: Int = 14 const val EVENT_PROJECTION_EVENT_COLOR_INDEX: Int = 15 + const val EVENT_PROJECTION_EVENT_COLOR_KEY_INDEX: Int = 16 val EVENT_PROJECTION: Array = arrayOf( CalendarContract.Instances.EVENT_ID, @@ -68,7 +69,8 @@ class Constants { CalendarContract.Events.EVENT_END_TIMEZONE, CalendarContract.Events.AVAILABILITY, CalendarContract.Events.STATUS, - CalendarContract.Events.EVENT_COLOR + CalendarContract.Events.EVENT_COLOR, + CalendarContract.Events.EVENT_COLOR_KEY ) const val EVENT_INSTANCE_DELETION_ID_INDEX: Int = 0 From e6e88e867444f568071900671355be10dd67e732 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Mon, 12 Aug 2024 18:12:35 +0200 Subject: [PATCH 32/43] mvoed color picker file --- .../lib/presentation/color_picker_dialog.dart | 28 +++++++++++++++++++ example/lib/presentation/pages/calendars.dart | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 example/lib/presentation/color_picker_dialog.dart diff --git a/example/lib/presentation/color_picker_dialog.dart b/example/lib/presentation/color_picker_dialog.dart new file mode 100644 index 00000000..04d7fc7d --- /dev/null +++ b/example/lib/presentation/color_picker_dialog.dart @@ -0,0 +1,28 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class ColorPickerDialog { + static Future selectColorDialog(List colors, BuildContext context) async { + return await showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: const Text('Select color'), + children: [ + ...colors.map((color) => + SimpleDialogOption( + onPressed: () { Navigator.pop(context, color); }, + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color), + ), + ) + )] + ); + } + ); + } +} \ No newline at end of file diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index 3c8bf1f5..5dc921bc 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:device_calendar/device_calendar.dart'; import 'package:device_calendar_example/presentation/pages/calendar_add.dart'; -import 'package:device_calendar_example/presentation/pages/color_picker_dialog.dart'; +import 'package:device_calendar_example/presentation/color_picker_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:collection/collection.dart'; From 35c49e62dbcc1a89d46966029c3e62c47cd5cbe4 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Mon, 12 Aug 2024 18:12:50 +0200 Subject: [PATCH 33/43] removed old color picker --- .../pages/color_picker_dialog.dart | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 example/lib/presentation/pages/color_picker_dialog.dart diff --git a/example/lib/presentation/pages/color_picker_dialog.dart b/example/lib/presentation/pages/color_picker_dialog.dart deleted file mode 100644 index 04d7fc7d..00000000 --- a/example/lib/presentation/pages/color_picker_dialog.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -class ColorPickerDialog { - static Future selectColorDialog(List colors, BuildContext context) async { - return await showDialog( - context: context, - builder: (BuildContext context) { - return SimpleDialog( - title: const Text('Select color'), - children: [ - ...colors.map((color) => - SimpleDialogOption( - onPressed: () { Navigator.pop(context, color); }, - child: Container( - width: 48, - height: 48, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: color), - ), - ) - )] - ); - } - ); - } -} \ No newline at end of file From 992240b2228f437b82688c257df3724a840f5564 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Wed, 4 Sep 2024 22:22:14 +0200 Subject: [PATCH 34/43] exposed color and colorKey --- lib/src/models/event.dart | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index 1c5fe91f..8b170910 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -51,16 +51,13 @@ class Event { EventStatus? status; /// Read-only. Color of the event - int? get color=> _color; - /// Read-only. Color of the event - int? get colorKey=> _colorKey; /// Only updatable for Android calendars where [DeviceCalendarPlugin.retrieveEventColors] returns an empty list. - int? _color; + int? color; /// Only updatable for colors of [DeviceCalendarPlugin.retrieveEventColors]. - int? _colorKey; + int? colorKey; ///Note for development: /// @@ -123,7 +120,7 @@ class Event { calendarId = json['calendarId']; title = json['eventTitle']; description = json['eventDescription']; - _color = json['eventColor']; + color = json['eventColor']; startTimestamp = json['eventStartDate']; startLocationName = json['eventStartTimeZone']; @@ -252,7 +249,7 @@ class Event { data['availability'] = availability.enumToString; data['eventStatus'] = status?.enumToString; data['eventColor'] = color; - data['eventColorKey'] = _colorKey; + data['eventColorKey'] = colorKey; if (attendees != null) { data['attendees'] = attendees?.map((a) => a?.toJson()).toList(); @@ -328,7 +325,7 @@ class Event { } void updateEventColor(EventColor? eventColor) { - _color = eventColor?.color; - _colorKey = eventColor?.colorKey; + color = eventColor?.color; + colorKey = eventColor?.colorKey; } } \ No newline at end of file From 44c248c9f083fa91cf2341549107fbce7570a0d7 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Wed, 11 Sep 2024 13:25:11 +0200 Subject: [PATCH 35/43] fixed color picker import --- example/lib/presentation/pages/calendar_event.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 523024e7..a447d33c 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -2,12 +2,12 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:device_calendar/device_calendar.dart'; -import 'package:device_calendar_example/presentation/pages/color_picker_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:intl/intl.dart'; +import '../color_picker_dialog.dart'; import '../date_time_picker.dart'; import '../recurring_event_dialog.dart'; import 'event_attendee.dart'; From ba0910b43a1271fe17ce118d6e8f92ed599047ea Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Wed, 11 Sep 2024 17:05:56 +0200 Subject: [PATCH 36/43] fixes for setting event color --- example/lib/presentation/pages/calendar_event.dart | 2 +- example/lib/presentation/pages/calendar_events.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index a447d33c..a4bc270b 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -304,7 +304,7 @@ class _CalendarEventPageState extends State { final colors = _eventColors?.map((eventColor) => Color(eventColor.color)).toList(); final newColor = await ColorPickerDialog.selectColorDialog(colors ?? [], context); setState(() { - _event?.updateEventColor(_eventColors?.firstWhereOrNull((eventColor) => eventColor.color == newColor?.value)); + _event?.updateEventColor(_eventColors?.firstWhereOrNull((eventColor) => Color(eventColor.color).value == newColor?.value)); }); } }, diff --git a/example/lib/presentation/pages/calendar_events.dart b/example/lib/presentation/pages/calendar_events.dart index 781b39b4..6b2e8384 100644 --- a/example/lib/presentation/pages/calendar_events.dart +++ b/example/lib/presentation/pages/calendar_events.dart @@ -79,7 +79,7 @@ class _CalendarEventsPageState extends State { onPressed: () async { final refreshEvents = await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { - return CalendarEventPage(_calendar); + return CalendarEventPage(_calendar, null, null, _eventColors); })); if (refreshEvents == true) { await _retrieveCalendarEvents(); From c702f17b663c963e6682d5d99246fe72fc82dad2 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Wed, 11 Sep 2024 17:18:44 +0200 Subject: [PATCH 37/43] clean up --- example/lib/presentation/pages/calendars.dart | 4 +++- lib/src/models/event.dart | 7 ++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index 5dc921bc..bc434173 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -63,7 +63,9 @@ class _CalendarsPageState extends State { itemCount: _calendars.length, itemBuilder: (BuildContext context, int index) { return GestureDetector( - key: ValueKey(_calendars[index].color), + key: Key(_calendars[index].isReadOnly == true + ? 'readOnlyCalendar${_readOnlyCalendars.indexWhere((c) => c.id == _calendars[index].id)} color:${_calendars[index].color}' + : 'writableCalendar${_writableCalendars.indexWhere((c) => c.id == _calendars[index].id)} color:${_calendars[index].color}'), onTap: () async { await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index 8b170910..d106eac1 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -50,13 +50,10 @@ class Event { /// Indicates if this event is of confirmed, canceled, tentative or none status EventStatus? status; - /// Read-only. Color of the event - - - /// Only updatable for Android calendars where [DeviceCalendarPlugin.retrieveEventColors] returns an empty list. + /// Read-only. Android exclusive. Updatable only using [Event.updateEventColor] with color from [DeviceCalendarPlugin.retrieveEventColors] int? color; - /// Only updatable for colors of [DeviceCalendarPlugin.retrieveEventColors]. + /// Read-only. Android exclusive. Updatable only using [Event.updateEventColor] with color from [DeviceCalendarPlugin.retrieveEventColors] int? colorKey; ///Note for development: From 2ee14a1035cc2e03a3478aa7f4e067fa531b54b5 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Wed, 11 Sep 2024 17:26:30 +0200 Subject: [PATCH 38/43] added serilization test for eventColor --- lib/src/models/event.dart | 1 + test/device_calendar_test.dart | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index d106eac1..eda68ffa 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -118,6 +118,7 @@ class Event { title = json['eventTitle']; description = json['eventDescription']; color = json['eventColor']; + colorKey = json['eventColorKey']; startTimestamp = json['eventStartDate']; startLocationName = json['eventStartTimeZone']; diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart index 132aad61..a2f1f1ca 100644 --- a/test/device_calendar_test.dart +++ b/test/device_calendar_test.dart @@ -216,7 +216,9 @@ void main() { recurrenceRule: recurrence, reminders: [reminder], availability: Availability.Busy, - status: EventStatus.Confirmed); + status: EventStatus.Confirmed, + ); + event.updateEventColor(EventColor(0xffff00ff, 1)); final stringEvent = event.toJson(); expect(stringEvent, isNotNull); @@ -241,5 +243,7 @@ void main() { expect(newEvent.reminders?.length, equals(1)); expect(newEvent.availability, equals(event.availability)); expect(newEvent.status, equals(event.status)); + expect(newEvent.color, equals(event.color)); + expect(newEvent.colorKey, equals(event.colorKey)); }); } From b41223cf6737c179bc2ea39bc47a01dbbe419e54 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Wed, 11 Sep 2024 17:56:16 +0200 Subject: [PATCH 39/43] tiny formatting adjustment --- test/device_calendar_test.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart index a2f1f1ca..0d91e738 100644 --- a/test/device_calendar_test.dart +++ b/test/device_calendar_test.dart @@ -216,8 +216,7 @@ void main() { recurrenceRule: recurrence, reminders: [reminder], availability: Availability.Busy, - status: EventStatus.Confirmed, - ); + status: EventStatus.Confirmed); event.updateEventColor(EventColor(0xffff00ff, 1)); final stringEvent = event.toJson(); From 787696271ec29fc41a300bc0f5a88da00682be79 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Wed, 11 Sep 2024 21:02:45 +0200 Subject: [PATCH 40/43] set example kotlin version to 1.8..22 --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index 34fcf1ae..73ccfc61 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,7 +2,7 @@ group 'com.builttoroam.devicecalendar' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.6.0' + ext.kotlin_version = '1.8.22' repositories { google() mavenCentral() From 755ddfa99832cf9bc722df3f472ec2438490c54a Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Fri, 13 Sep 2024 21:13:51 +0200 Subject: [PATCH 41/43] flutter_native_timezone made release builds fail, migrated to flutter_timezone --- example/lib/presentation/event_item.dart | 4 ++-- example/lib/presentation/pages/calendar_event.dart | 4 ++-- example/pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/example/lib/presentation/event_item.dart b/example/lib/presentation/event_item.dart index f91bb7de..b18d299d 100644 --- a/example/lib/presentation/event_item.dart +++ b/example/lib/presentation/event_item.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_native_timezone/flutter_native_timezone.dart'; +import 'package:flutter_timezone/flutter_timezone.dart'; import 'package:intl/intl.dart'; import 'recurring_event_dialog.dart'; @@ -313,7 +313,7 @@ class _EventItemState extends State { void setCurentLocation() async { String? timezone; try { - timezone = await FlutterNativeTimezone.getLocalTimezone(); + timezone = await FlutterTimezone.getLocalTimezone(); } catch (e) { debugPrint('Could not get the local timezone'); } diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index a4bc270b..83ad1a23 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -4,7 +4,7 @@ import 'package:collection/collection.dart'; import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_native_timezone/flutter_native_timezone.dart'; +import 'package:flutter_timezone/flutter_timezone.dart'; import 'package:intl/intl.dart'; import '../color_picker_dialog.dart'; @@ -73,7 +73,7 @@ class _CalendarEventPageState extends State { void getCurentLocation() async { try { - _timezone = await FlutterNativeTimezone.getLocalTimezone(); + _timezone = await FlutterTimezone.getLocalTimezone(); } catch (e) { debugPrint('Could not get the local timezone'); } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b32d2fb0..9ffb81f4 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: sdk: flutter intl: ^0.17.0 uuid: ^3.0.6 - flutter_native_timezone: ^2.0.0 + flutter_timezone: ^3.0.1 device_calendar: path: ../ From 7791f596754213df3fa871a0c0316e139c78c305 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Wed, 25 Sep 2024 20:51:04 +0200 Subject: [PATCH 42/43] increased rrule version from 0.2.10 to 0.2.15 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 7c99a9c5..101681fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ dependencies: sdk: flutter collection: ^1.16.0 timezone: ^0.9.0 - rrule: ^0.2.10 + rrule: ^0.2.15 dev_dependencies: flutter_test: From 2d8eddf0fdd96760a77530627d512f1caf440ebf Mon Sep 17 00:00:00 2001 From: Jonathan de Gaston Date: Fri, 7 Mar 2025 10:16:10 -0700 Subject: [PATCH 43/43] Fix isAllDay null error on iOS When an Event object is created in flutter with null allDay value the value will default to false on iOS like it does on Android. --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index 323c8f5a..8c302f49 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -841,7 +841,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let arguments = call.arguments as! Dictionary let calendarId = arguments[calendarIdArgument] as! String let eventId = arguments[eventIdArgument] as? String - let isAllDay = arguments[eventAllDayArgument] as! Bool + let isAllDay = (arguments[eventAllDayArgument] as? Bool) ?? false let startDateMillisecondsSinceEpoch = arguments[eventStartDateArgument] as! NSNumber let endDateDateMillisecondsSinceEpoch = arguments[eventEndDateArgument] as! NSNumber let startDate = Date (timeIntervalSince1970: startDateMillisecondsSinceEpoch.doubleValue / 1000.0)