From 40b113534ca14c8a5f54d288b5f5197b16ee3fe0 Mon Sep 17 00:00:00 2001 From: GoldenSoju <42365471+GoldenSoju@users.noreply.github.com> Date: Thu, 6 Oct 2022 13:47:20 +0900 Subject: [PATCH 01/45] Implementing Rrule package (#403) Co-authored-by: GoldenSoju Co-authored-by: thomassth --- android/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 3 +- .../devicecalendar/AvailabilitySerializer.kt | 10 +- .../devicecalendar/CalendarDelegate.kt | 851 +++++--- .../devicecalendar/DayOfWeekSerializer.kt | 14 - .../devicecalendar/DeviceCalendarPlugin.kt | 196 +- .../RecurrenceFrequencySerializer.kt | 15 - .../devicecalendar/common/Constants.kt | 58 +- .../devicecalendar/common/DayOfWeek.kt | 11 - .../devicecalendar/common/ErrorMessages.kt | 15 +- .../common/RecurrenceFrequency.kt | 5 - .../devicecalendar/models/Attendee.kt | 9 +- .../devicecalendar/models/Calendar.kt | 9 +- .../CalendarMethodsParametersCacheModel.kt | 18 +- .../devicecalendar/models/Event.kt | 1 - .../devicecalendar/models/RecurrenceRule.kt | 22 +- example/analysis_options.yaml | 4 +- example/ios/Podfile.lock | 2 +- example/ios/Runner.xcodeproj/project.pbxproj | 6 +- example/lib/presentation/event_item.dart | 12 +- .../lib/presentation/pages/calendar_add.dart | 1 - .../presentation/pages/calendar_event.dart | 1703 +++++++++-------- .../presentation/pages/calendar_events.dart | 7 +- example/lib/presentation/pages/calendars.dart | 6 +- .../presentation/pages/event_attendee.dart | 4 +- example/pubspec.yaml | 61 +- ios/Classes/SwiftDeviceCalendarPlugin.swift | 1005 +++++----- lib/device_calendar.dart | 4 +- lib/src/common/calendar_enums.dart | 8 +- lib/src/common/recurrence_frequency.dart | 6 - lib/src/device_calendar.dart | 110 +- lib/src/models/event.dart | 47 +- lib/src/models/recurrence_rule.dart | 132 -- pubspec.yaml | 5 +- test/device_calendar_test.dart | 6 +- 35 files changed, 2396 insertions(+), 1972 deletions(-) delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/DayOfWeekSerializer.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/RecurrenceFrequencySerializer.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/common/DayOfWeek.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/common/RecurrenceFrequency.kt delete mode 100644 lib/src/common/recurrence_frequency.dart delete mode 100644 lib/src/models/recurrence_rule.dart diff --git a/android/build.gradle b/android/build.gradle index eb0783b7..66d86743 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -51,6 +51,6 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.google.code.gson:gson:2.8.8' api 'androidx.appcompat:appcompat:1.3.1' - implementation 'org.dmfs:lib-recur:0.11.2' + implementation 'org.dmfs:lib-recur:0.12.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 2212bc37..3c9d0852 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Thu May 17 10:56:13 AEST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt index 707b9286..5a803a6b 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt @@ -4,9 +4,13 @@ import com.builttoroam.devicecalendar.models.Availability import com.google.gson.* import java.lang.reflect.Type -class AvailabilitySerializer: JsonSerializer { - override fun serialize(src: Availability?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { - if(src != null) { +class AvailabilitySerializer : JsonSerializer { + override fun serialize( + src: Availability?, + typeOfSrc: Type?, + context: JsonSerializationContext? + ): JsonElement { + if (src != null) { return JsonPrimitive(src.name) } return JsonObject() diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt index 744f636e..1cd3f98a 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -10,62 +10,14 @@ import android.content.pm.PackageManager import android.database.Cursor import android.graphics.Color import android.net.Uri +import android.os.Build import android.os.Handler import android.os.Looper import android.provider.CalendarContract import android.provider.CalendarContract.CALLER_IS_SYNCADAPTER import android.provider.CalendarContract.Events import android.text.format.DateUtils -import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_EMAIL_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_NAME_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_PROJECTION -import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_RELATIONSHIP_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_STATUS_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_TYPE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_ACCESS_LEVEL_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_ACCOUNT_NAME_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_ACCOUNT_TYPE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_COLOR_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_DISPLAY_NAME_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_ID_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_IS_PRIMARY_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_OLDER_API -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_OWNER_ACCOUNT_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_BEGIN_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_END_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_ID_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_LAST_DATE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_RRULE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_ALL_DAY_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_AVAILABILITY_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_BEGIN_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_CUSTOM_APP_URI_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_DESCRIPTION_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_END_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_END_TIMEZONE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_EVENT_LOCATION_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_ID_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_RECURRING_RULE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_START_TIMEZONE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_STATUS_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_TITLE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.REMINDER_MINUTES_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.REMINDER_PROJECTION -import com.builttoroam.devicecalendar.common.DayOfWeek -import com.builttoroam.devicecalendar.common.ErrorCodes.Companion.GENERIC_ERROR -import com.builttoroam.devicecalendar.common.ErrorCodes.Companion.INVALID_ARGUMENT -import com.builttoroam.devicecalendar.common.ErrorCodes.Companion.NOT_ALLOWED -import com.builttoroam.devicecalendar.common.ErrorCodes.Companion.NOT_AUTHORIZED -import com.builttoroam.devicecalendar.common.ErrorCodes.Companion.NOT_FOUND import com.builttoroam.devicecalendar.common.ErrorMessages -import com.builttoroam.devicecalendar.common.ErrorMessages.Companion.CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE -import com.builttoroam.devicecalendar.common.ErrorMessages.Companion.CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE -import com.builttoroam.devicecalendar.common.ErrorMessages.Companion.EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE -import com.builttoroam.devicecalendar.common.ErrorMessages.Companion.NOT_AUTHORIZED_MESSAGE -import com.builttoroam.devicecalendar.common.RecurrenceFrequency import com.builttoroam.devicecalendar.models.* import com.builttoroam.devicecalendar.models.Calendar import com.google.gson.Gson @@ -75,44 +27,52 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.PluginRegistry import kotlinx.coroutines.* import org.dmfs.rfc5545.DateTime +import org.dmfs.rfc5545.DateTime.UTC import org.dmfs.rfc5545.Weekday -import org.dmfs.rfc5545.recur.Freq -import java.text.SimpleDateFormat +import org.dmfs.rfc5545.recur.RecurrenceRule.WeekdayNum import java.util.* - -class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { - private val RETRIEVE_CALENDARS_REQUEST_CODE = 0 - private val RETRIEVE_EVENTS_REQUEST_CODE = RETRIEVE_CALENDARS_REQUEST_CODE + 1 - private val RETRIEVE_CALENDAR_REQUEST_CODE = RETRIEVE_EVENTS_REQUEST_CODE + 1 - private val CREATE_OR_UPDATE_EVENT_REQUEST_CODE = RETRIEVE_CALENDAR_REQUEST_CODE + 1 - private val DELETE_EVENT_REQUEST_CODE = CREATE_OR_UPDATE_EVENT_REQUEST_CODE + 1 - private val REQUEST_PERMISSIONS_REQUEST_CODE = DELETE_EVENT_REQUEST_CODE + 1 - private val DELETE_CALENDAR_REQUEST_CODE = REQUEST_PERMISSIONS_REQUEST_CODE + 1 - private val PART_TEMPLATE = ";%s=" - private val BYMONTHDAY_PART = "BYMONTHDAY" - private val BYMONTH_PART = "BYMONTH" - private val BYSETPOS_PART = "BYSETPOS" - - private val _cachedParametersMap: MutableMap = mutableMapOf() - private var _binding: ActivityPluginBinding? = null - private var _context: Context? = null +import kotlin.math.absoluteValue +import kotlin.time.DurationUnit +import kotlin.time.toDuration +import com.builttoroam.devicecalendar.common.Constants.Companion as Cst +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 + +private const val RETRIEVE_CALENDARS_REQUEST_CODE = 0 +private const val RETRIEVE_EVENTS_REQUEST_CODE = RETRIEVE_CALENDARS_REQUEST_CODE + 1 +private const val RETRIEVE_CALENDAR_REQUEST_CODE = RETRIEVE_EVENTS_REQUEST_CODE + 1 +private const val CREATE_OR_UPDATE_EVENT_REQUEST_CODE = RETRIEVE_CALENDAR_REQUEST_CODE + 1 +private const val DELETE_EVENT_REQUEST_CODE = CREATE_OR_UPDATE_EVENT_REQUEST_CODE + 1 +private const val REQUEST_PERMISSIONS_REQUEST_CODE = DELETE_EVENT_REQUEST_CODE + 1 +private const val DELETE_CALENDAR_REQUEST_CODE = REQUEST_PERMISSIONS_REQUEST_CODE + 1 + +class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : + PluginRegistry.RequestPermissionsResultListener { + + private val _cachedParametersMap: MutableMap = + mutableMapOf() + private var _binding: ActivityPluginBinding? = binding + private var _context: Context? = context private var _gson: Gson? = null private val uiThreadHandler = Handler(Looper.getMainLooper()) - constructor(binding: ActivityPluginBinding?, context: Context) { - _binding = binding - _context = context + init { val gsonBuilder = GsonBuilder() - gsonBuilder.registerTypeAdapter(RecurrenceFrequency::class.java, RecurrenceFrequencySerializer()) - gsonBuilder.registerTypeAdapter(DayOfWeek::class.java, DayOfWeekSerializer()) gsonBuilder.registerTypeAdapter(Availability::class.java, AvailabilitySerializer()) gsonBuilder.registerTypeAdapter(EventStatus::class.java, EventStatusSerializer()) _gson = gsonBuilder.create() } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray): Boolean { - val permissionGranted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ): Boolean { + val permissionGranted = + grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED if (!_cachedParametersMap.containsKey(requestCode)) { // this plugin doesn't handle this request code @@ -120,13 +80,17 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } val cachedValues: CalendarMethodsParametersCacheModel = _cachedParametersMap[requestCode] - ?: // unlikely scenario where another plugin is potentially using the same request code but it's not one we are tracking so return to - // indicate we're not handling the request - return false + ?: // unlikely scenario where another plugin is potentially using the same request code but it's not one we are tracking so return to + // indicate we're not handling the request + return false try { if (!permissionGranted) { - finishWithError(NOT_AUTHORIZED, NOT_AUTHORIZED_MESSAGE, cachedValues.pendingChannelResult) + finishWithError( + EC.NOT_AUTHORIZED, + EM.NOT_AUTHORIZED_MESSAGE, + cachedValues.pendingChannelResult + ) return false } @@ -135,22 +99,36 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { retrieveCalendars(cachedValues.pendingChannelResult) } RETRIEVE_EVENTS_REQUEST_CODE -> { - retrieveEvents(cachedValues.calendarId, cachedValues.calendarEventsStartDate, cachedValues.calendarEventsEndDate, cachedValues.calendarEventsIds, cachedValues.pendingChannelResult) + retrieveEvents( + cachedValues.calendarId, + cachedValues.calendarEventsStartDate, + cachedValues.calendarEventsEndDate, + cachedValues.calendarEventsIds, + cachedValues.pendingChannelResult + ) } RETRIEVE_CALENDAR_REQUEST_CODE -> { retrieveCalendar(cachedValues.calendarId, cachedValues.pendingChannelResult) } CREATE_OR_UPDATE_EVENT_REQUEST_CODE -> { - createOrUpdateEvent(cachedValues.calendarId, cachedValues.event, cachedValues.pendingChannelResult) + createOrUpdateEvent( + cachedValues.calendarId, + cachedValues.event, + cachedValues.pendingChannelResult + ) } DELETE_EVENT_REQUEST_CODE -> { - deleteEvent(cachedValues.calendarId, cachedValues.eventId, cachedValues.pendingChannelResult) + deleteEvent( + cachedValues.calendarId, + cachedValues.eventId, + cachedValues.pendingChannelResult + ) } REQUEST_PERMISSIONS_REQUEST_CODE -> { finishWithSuccess(permissionGranted, cachedValues.pendingChannelResult) } DELETE_CALENDAR_REQUEST_CODE -> { - deleteCalendar(cachedValues.calendarId,cachedValues.pendingChannelResult) + deleteCalendar(cachedValues.calendarId, cachedValues.pendingChannelResult) } } @@ -164,7 +142,10 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { if (arePermissionsGranted()) { finishWithSuccess(true, pendingChannelResult) } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, REQUEST_PERMISSIONS_REQUEST_CODE) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + REQUEST_PERMISSIONS_REQUEST_CODE + ) requestPermissions(parameters) } } @@ -179,9 +160,9 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { val contentResolver: ContentResolver? = _context?.contentResolver val uri: Uri = CalendarContract.Calendars.CONTENT_URI val cursor: Cursor? = if (atLeastAPI(17)) { - contentResolver?.query(uri, CALENDAR_PROJECTION, null, null, null) + contentResolver?.query(uri, Cst.CALENDAR_PROJECTION, null, null, null) } else { - contentResolver?.query(uri, CALENDAR_PROJECTION_OLDER_API, null, null, null) + contentResolver?.query(uri, Cst.CALENDAR_PROJECTION_OLDER_API, null, null, null) } val calendars: MutableList = mutableListOf() try { @@ -192,22 +173,33 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { finishWithSuccess(_gson?.toJson(calendars), pendingChannelResult) } catch (e: Exception) { - finishWithError(GENERIC_ERROR, e.message, pendingChannelResult) + finishWithError(EC.GENERIC_ERROR, e.message, pendingChannelResult) } finally { cursor?.close() } } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, RETRIEVE_CALENDARS_REQUEST_CODE) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + RETRIEVE_CALENDARS_REQUEST_CODE + ) requestPermissions(parameters) } } - private fun retrieveCalendar(calendarId: String, pendingChannelResult: MethodChannel.Result, isInternalCall: Boolean = false): Calendar? { + private fun retrieveCalendar( + calendarId: String, + pendingChannelResult: MethodChannel.Result, + isInternalCall: Boolean = false + ): Calendar? { if (isInternalCall || arePermissionsGranted()) { val calendarIdNumber = calendarId.toLongOrNull() if (calendarIdNumber == null) { if (!isInternalCall) { - finishWithError(INVALID_ARGUMENT, CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE, pendingChannelResult) + finishWithError( + EC.INVALID_ARGUMENT, + EM.CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE, + pendingChannelResult + ) } return null } @@ -216,9 +208,21 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { val uri: Uri = CalendarContract.Calendars.CONTENT_URI val cursor: Cursor? = if (atLeastAPI(17)) { - contentResolver?.query(ContentUris.withAppendedId(uri, calendarIdNumber), CALENDAR_PROJECTION, null, null, null) + contentResolver?.query( + ContentUris.withAppendedId(uri, calendarIdNumber), + Cst.CALENDAR_PROJECTION, + null, + null, + null + ) } else { - contentResolver?.query(ContentUris.withAppendedId(uri, calendarIdNumber), CALENDAR_PROJECTION_OLDER_API, null, null, null) + contentResolver?.query( + ContentUris.withAppendedId(uri, calendarIdNumber), + Cst.CALENDAR_PROJECTION_OLDER_API, + null, + null, + null + ) } try { @@ -231,74 +235,116 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } } else { if (!isInternalCall) { - finishWithError(NOT_FOUND, "The calendar with the ID $calendarId could not be found", pendingChannelResult) + finishWithError( + EC.NOT_FOUND, + "The calendar with the ID $calendarId could not be found", + pendingChannelResult + ) } } } catch (e: Exception) { - finishWithError(GENERIC_ERROR, e.message, pendingChannelResult) + finishWithError(EC.GENERIC_ERROR, e.message, pendingChannelResult) } finally { cursor?.close() } } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, RETRIEVE_CALENDAR_REQUEST_CODE, calendarId) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + RETRIEVE_CALENDAR_REQUEST_CODE, + calendarId + ) requestPermissions(parameters) } return null } - fun deleteCalendar(calendarId: String, pendingChannelResult: MethodChannel.Result, isInternalCall: Boolean = false): Calendar? { + fun deleteCalendar( + calendarId: String, + pendingChannelResult: MethodChannel.Result, + isInternalCall: Boolean = false + ): Calendar? { if (isInternalCall || arePermissionsGranted()) { val calendarIdNumber = calendarId.toLongOrNull() if (calendarIdNumber == null) { if (!isInternalCall) { - finishWithError(INVALID_ARGUMENT, CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE, pendingChannelResult) + finishWithError( + EC.INVALID_ARGUMENT, + EM.CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE, + pendingChannelResult + ) } return null } val contentResolver: ContentResolver? = _context?.contentResolver - val calendar = retrieveCalendar(calendarId,pendingChannelResult,true); - if(calendar != null) { - val calenderUriWithId = ContentUris.withAppendedId(CalendarContract.Calendars.CONTENT_URI, calendarIdNumber) + val calendar = retrieveCalendar(calendarId, pendingChannelResult, true) + if (calendar != null) { + val calenderUriWithId = ContentUris.withAppendedId( + CalendarContract.Calendars.CONTENT_URI, + calendarIdNumber + ) val deleteSucceeded = contentResolver?.delete(calenderUriWithId, null, null) ?: 0 finishWithSuccess(deleteSucceeded > 0, pendingChannelResult) - }else { + } else { if (!isInternalCall) { - finishWithError(NOT_FOUND, "The calendar with the ID $calendarId could not be found", pendingChannelResult) + finishWithError( + EC.NOT_FOUND, + "The calendar with the ID $calendarId could not be found", + pendingChannelResult + ) } } } else { val parameters = CalendarMethodsParametersCacheModel( - pendingChannelResult = pendingChannelResult, - calendarDelegateMethodCode = DELETE_CALENDAR_REQUEST_CODE, - calendarId = calendarId) + pendingChannelResult = pendingChannelResult, + calendarDelegateMethodCode = DELETE_CALENDAR_REQUEST_CODE, + calendarId = calendarId + ) requestPermissions(parameters) } return null } - fun createCalendar(calendarName: String, calendarColor: String?, localAccountName: String, pendingChannelResult: MethodChannel.Result) { + fun createCalendar( + calendarName: String, + calendarColor: String?, + localAccountName: String, + pendingChannelResult: MethodChannel.Result + ) { val contentResolver: ContentResolver? = _context?.contentResolver var uri = CalendarContract.Calendars.CONTENT_URI uri = uri.buildUpon() - .appendQueryParameter(CALLER_IS_SYNCADAPTER, "true") - .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, localAccountName) - .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL) - .build() + .appendQueryParameter(CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, localAccountName) + .appendQueryParameter( + CalendarContract.Calendars.ACCOUNT_TYPE, + CalendarContract.ACCOUNT_TYPE_LOCAL + ) + .build() val values = ContentValues() values.put(CalendarContract.Calendars.NAME, calendarName) values.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, calendarName) values.put(CalendarContract.Calendars.ACCOUNT_NAME, localAccountName) values.put(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL) - values.put(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, CalendarContract.Calendars.CAL_ACCESS_OWNER) - values.put(CalendarContract.Calendars.CALENDAR_COLOR, Color.parseColor((calendarColor - ?: "0xFFFF0000").replace("0x", "#"))) // Red colour as a default + values.put( + CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, + CalendarContract.Calendars.CAL_ACCESS_OWNER + ) + values.put( + CalendarContract.Calendars.CALENDAR_COLOR, Color.parseColor( + (calendarColor + ?: "0xFFFF0000").replace("0x", "#") + ) + ) // Red colour as a default values.put(CalendarContract.Calendars.OWNER_ACCOUNT, localAccountName) - values.put(CalendarContract.Calendars.CALENDAR_TIME_ZONE, java.util.Calendar.getInstance().timeZone.id) + values.put( + CalendarContract.Calendars.CALENDAR_TIME_ZONE, + java.util.Calendar.getInstance().timeZone.id + ) val result = contentResolver?.insert(uri, values) // Get the calendar ID that is the last element in the Uri @@ -307,16 +353,30 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { finishWithSuccess(calendarId.toString(), pendingChannelResult) } - fun retrieveEvents(calendarId: String, startDate: Long?, endDate: Long?, eventIds: List, pendingChannelResult: MethodChannel.Result) { + fun retrieveEvents( + calendarId: String, + startDate: Long?, + endDate: Long?, + eventIds: List, + pendingChannelResult: MethodChannel.Result + ) { if (startDate == null && endDate == null && eventIds.isEmpty()) { - finishWithError(INVALID_ARGUMENT, ErrorMessages.RETRIEVE_EVENTS_ARGUMENTS_NOT_VALID_MESSAGE, pendingChannelResult) + finishWithError( + EC.INVALID_ARGUMENT, + ErrorMessages.RETRIEVE_EVENTS_ARGUMENTS_NOT_VALID_MESSAGE, + pendingChannelResult + ) return } if (arePermissionsGranted()) { val calendar = retrieveCalendar(calendarId, pendingChannelResult, true) if (calendar == null) { - finishWithError(NOT_FOUND, "Couldn't retrieve the Calendar with ID $calendarId", pendingChannelResult) + finishWithError( + EC.NOT_FOUND, + "Couldn't retrieve the Calendar with ID $calendarId", + pendingChannelResult + ) return } @@ -328,7 +388,8 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { val eventsUri = eventsUriBuilder.build() val eventsCalendarQuery = "(${Events.CALENDAR_ID} = $calendarId)" val eventsNotDeletedQuery = "(${Events.DELETED} != 1)" - val eventsIdsQuery = "(${CalendarContract.Instances.EVENT_ID} IN (${eventIds.joinToString()}))" + val eventsIdsQuery = + "(${CalendarContract.Instances.EVENT_ID} IN (${eventIds.joinToString()}))" var eventsSelectionQuery = "$eventsCalendarQuery AND $eventsNotDeletedQuery" if (eventIds.isNotEmpty()) { @@ -336,13 +397,19 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } val eventsSortOrder = Events.DTSTART + " DESC" - val eventsCursor = contentResolver?.query(eventsUri, EVENT_PROJECTION, eventsSelectionQuery, null, eventsSortOrder) + val eventsCursor = contentResolver?.query( + eventsUri, + Cst.EVENT_PROJECTION, + eventsSelectionQuery, + null, + eventsSortOrder + ) val events: MutableList = mutableListOf() val exceptionHandler = CoroutineExceptionHandler { _, exception -> uiThreadHandler.post { - finishWithError(GENERIC_ERROR, exception.message, pendingChannelResult) + finishWithError(EC.GENERIC_ERROR, exception.message, pendingChannelResult) } } @@ -353,7 +420,8 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } for (event in events) { val attendees = retrieveAttendees(calendar, event.eventId!!, contentResolver) - event.organizer = attendees.firstOrNull { it.isOrganizer != null && it.isOrganizer } + event.organizer = + attendees.firstOrNull { it.isOrganizer != null && it.isOrganizer } event.attendees = attendees event.reminders = retrieveReminders(event.eventId!!, contentResolver) } @@ -366,23 +434,41 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } } } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, RETRIEVE_EVENTS_REQUEST_CODE, calendarId, startDate, endDate) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + RETRIEVE_EVENTS_REQUEST_CODE, + calendarId, + startDate, + endDate + ) requestPermissions(parameters) } return } - fun createOrUpdateEvent(calendarId: String, event: Event?, pendingChannelResult: MethodChannel.Result) { + fun createOrUpdateEvent( + calendarId: String, + event: Event?, + pendingChannelResult: MethodChannel.Result + ) { if (arePermissionsGranted()) { if (event == null) { - finishWithError(GENERIC_ERROR, CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE, pendingChannelResult) + finishWithError( + EC.GENERIC_ERROR, + EM.CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE, + pendingChannelResult + ) return } val calendar = retrieveCalendar(calendarId, pendingChannelResult, true) if (calendar == null) { - finishWithError(NOT_FOUND, "Couldn't retrieve the Calendar with ID $calendarId", pendingChannelResult) + finishWithError( + EC.NOT_FOUND, + "Couldn't retrieve the Calendar with ID $calendarId", + pendingChannelResult + ) return } @@ -391,7 +477,7 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { val exceptionHandler = CoroutineExceptionHandler { _, exception -> uiThreadHandler.post { - finishWithError(GENERIC_ERROR, exception.message, pendingChannelResult) + finishWithError(EC.GENERIC_ERROR, exception.message, pendingChannelResult) } } @@ -407,14 +493,22 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } } else { job = GlobalScope.launch(Dispatchers.IO + exceptionHandler) { - contentResolver?.update(ContentUris.withAppendedId(Events.CONTENT_URI, eventId), values, null, null) - val existingAttendees = retrieveAttendees(calendar, eventId.toString(), contentResolver) - val attendeesToDelete = if (event.attendees.isNotEmpty()) existingAttendees.filter { existingAttendee -> event.attendees.all { it.emailAddress != existingAttendee.emailAddress } } else existingAttendees + contentResolver?.update( + ContentUris.withAppendedId(Events.CONTENT_URI, eventId), + values, + null, + null + ) + val existingAttendees = + retrieveAttendees(calendar, eventId.toString(), contentResolver) + val attendeesToDelete = + if (event.attendees.isNotEmpty()) existingAttendees.filter { existingAttendee -> event.attendees.all { it.emailAddress != existingAttendee.emailAddress } } else existingAttendees for (attendeeToDelete in attendeesToDelete) { deleteAttendee(eventId, attendeeToDelete, contentResolver) } - val attendeesToInsert = event.attendees.filter { existingAttendees.all { existingAttendee -> existingAttendee.emailAddress != it.emailAddress } } + val attendeesToInsert = + event.attendees.filter { existingAttendees.all { existingAttendee -> existingAttendee.emailAddress != it.emailAddress } } insertAttendees(attendeesToInsert, eventId, contentResolver) deleteExistingReminders(contentResolver, eventId) insertReminders(event.reminders, eventId, contentResolver!!) @@ -427,13 +521,13 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } if (existingSelfAttendee != null && newSelfAttendee != null && newSelfAttendee.attendanceStatus != null && - existingSelfAttendee.attendanceStatus != newSelfAttendee.attendanceStatus) { + existingSelfAttendee.attendanceStatus != newSelfAttendee.attendanceStatus + ) { updateAttendeeStatus(eventId, newSelfAttendee, contentResolver) } } } - job.invokeOnCompletion { - cause -> + job.invokeOnCompletion { cause -> if (cause == null) { uiThreadHandler.post { finishWithSuccess(eventId.toString(), pendingChannelResult) @@ -441,21 +535,28 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } } } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, CREATE_OR_UPDATE_EVENT_REQUEST_CODE, calendarId) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + CREATE_OR_UPDATE_EVENT_REQUEST_CODE, + calendarId + ) parameters.event = event requestPermissions(parameters) } } private fun deleteExistingReminders(contentResolver: ContentResolver?, eventId: Long) { - val cursor = CalendarContract.Reminders.query(contentResolver, eventId, arrayOf( - CalendarContract.Reminders._ID - )) + val cursor = CalendarContract.Reminders.query( + contentResolver, eventId, arrayOf( + CalendarContract.Reminders._ID + ) + ) while (cursor != null && cursor.moveToNext()) { var reminderUri: Uri? = null val reminderId = cursor.getLong(0) if (reminderId > 0) { - reminderUri = ContentUris.withAppendedId(CalendarContract.Reminders.CONTENT_URI, reminderId) + reminderUri = + ContentUris.withAppendedId(CalendarContract.Reminders.CONTENT_URI, reminderId) } if (reminderUri != null) { contentResolver?.delete(reminderUri, null, null) @@ -465,7 +566,11 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } @SuppressLint("MissingPermission") - private fun insertReminders(reminders: List, eventId: Long?, contentResolver: ContentResolver) { + private fun insertReminders( + reminders: List, + eventId: Long?, + contentResolver: ContentResolver + ) { if (reminders.isEmpty()) { return } @@ -481,25 +586,45 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { private fun buildEventContentValues(event: Event, calendarId: String): ContentValues { val values = ContentValues() - val duration: String? = null - values.put(Events.ALL_DAY, event.eventAllDay) + + values.put(Events.ALL_DAY, if (event.eventAllDay) 1 else 0) values.put(Events.DTSTART, event.eventStartDate!!) values.put(Events.EVENT_TIMEZONE, getTimeZone(event.eventStartTimeZone).id) - values.put(Events.DTEND, event.eventEndDate!!) - values.put(Events.EVENT_END_TIMEZONE, getTimeZone(event.eventEndTimeZone).id) values.put(Events.TITLE, event.eventTitle) values.put(Events.DESCRIPTION, event.eventDescription) values.put(Events.EVENT_LOCATION, event.eventLocation) values.put(Events.CUSTOM_APP_URI, event.eventURL) values.put(Events.CALENDAR_ID, calendarId) - values.put(Events.DURATION, duration) values.put(Events.AVAILABILITY, getAvailability(event.availability)) - values.put(Events.STATUS, getEventStatus(event.eventStatus)) + var status: Int? = getEventStatus(event.eventStatus) + if (status != null) { + values.put(Events.STATUS, status) + } + + var duration: String? = null + var end: Long? = null + var endTimeZone: String? = null if (event.recurrenceRule != null) { val recurrenceRuleParams = buildRecurrenceRuleParams(event.recurrenceRule!!) values.put(Events.RRULE, recurrenceRuleParams) + val difference = event.eventEndDate!!.minus(event.eventStartDate!!) + val rawDuration = difference.toDuration(DurationUnit.MILLISECONDS) + rawDuration.toComponents { days, hours, minutes, seconds, _ -> + if (days > 0 || hours > 0 || minutes > 0 || seconds > 0) duration = "P" + if (days > 0) duration = duration.plus("${days}D") + if (hours > 0 || minutes > 0 || seconds > 0) duration = duration.plus("T") + if (hours > 0) duration = duration.plus("${hours}H") + if (minutes > 0) duration = duration.plus("${minutes}M") + if (seconds > 0) duration = duration.plus("${seconds}S") + } + } else { + end = event.eventEndDate!! + endTimeZone = getTimeZone(event.eventEndTimeZone).id } + values.put(Events.DTEND, end) + values.put(Events.EVENT_END_TIMEZONE, endTimeZone) + values.put(Events.DURATION, duration) return values } @@ -530,7 +655,11 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } @SuppressLint("MissingPermission") - private fun insertAttendees(attendees: List, eventId: Long?, contentResolver: ContentResolver?) { + private fun insertAttendees( + attendees: List, + eventId: Long?, + contentResolver: ContentResolver? + ) { if (attendees.isEmpty()) { return } @@ -540,13 +669,13 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { put(CalendarContract.Attendees.ATTENDEE_NAME, it.name) put(CalendarContract.Attendees.ATTENDEE_EMAIL, it.emailAddress) put( - CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, - CalendarContract.Attendees.RELATIONSHIP_ATTENDEE + CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, + CalendarContract.Attendees.RELATIONSHIP_ATTENDEE ) put(CalendarContract.Attendees.ATTENDEE_TYPE, it.role) put( - CalendarContract.Attendees.ATTENDEE_STATUS, - it.attendanceStatus + CalendarContract.Attendees.ATTENDEE_STATUS, + it.attendanceStatus ) put(CalendarContract.Attendees.EVENT_ID, eventId) } @@ -556,37 +685,71 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } @SuppressLint("MissingPermission") - private fun deleteAttendee(eventId: Long, attendee: Attendee, contentResolver: ContentResolver?) { - val selection = "(" + CalendarContract.Attendees.EVENT_ID + " = ?) AND (" + CalendarContract.Attendees.ATTENDEE_EMAIL + " = ?)" + private fun deleteAttendee( + eventId: Long, + attendee: Attendee, + contentResolver: ContentResolver? + ) { + val selection = + "(" + CalendarContract.Attendees.EVENT_ID + " = ?) AND (" + CalendarContract.Attendees.ATTENDEE_EMAIL + " = ?)" val selectionArgs = arrayOf(eventId.toString() + "", attendee.emailAddress) contentResolver?.delete(CalendarContract.Attendees.CONTENT_URI, selection, selectionArgs) } - private fun updateAttendeeStatus(eventId: Long, attendee: Attendee, contentResolver: ContentResolver?) { - val selection = "(" + CalendarContract.Attendees.EVENT_ID + " = ?) AND (" + CalendarContract.Attendees.ATTENDEE_EMAIL + " = ?)" + private fun updateAttendeeStatus( + eventId: Long, + attendee: Attendee, + contentResolver: ContentResolver? + ) { + val selection = + "(" + CalendarContract.Attendees.EVENT_ID + " = ?) AND (" + CalendarContract.Attendees.ATTENDEE_EMAIL + " = ?)" val selectionArgs = arrayOf(eventId.toString() + "", attendee.emailAddress) val values = ContentValues() values.put(CalendarContract.Attendees.ATTENDEE_STATUS, attendee.attendanceStatus) - contentResolver?.update(CalendarContract.Attendees.CONTENT_URI, values, selection, selectionArgs) + contentResolver?.update( + CalendarContract.Attendees.CONTENT_URI, + values, + selection, + selectionArgs + ) } - fun deleteEvent(calendarId: String, eventId: String, pendingChannelResult: MethodChannel.Result, startDate: Long? = null, endDate: Long? = null, followingInstances: Boolean? = null) { + fun deleteEvent( + calendarId: String, + eventId: String, + pendingChannelResult: MethodChannel.Result, + startDate: Long? = null, + endDate: Long? = null, + followingInstances: Boolean? = null + ) { if (arePermissionsGranted()) { val existingCal = retrieveCalendar(calendarId, pendingChannelResult, true) if (existingCal == null) { - finishWithError(NOT_FOUND, "The calendar with the ID $calendarId could not be found", pendingChannelResult) + finishWithError( + EC.NOT_FOUND, + "The calendar with the ID $calendarId could not be found", + pendingChannelResult + ) return } if (existingCal.isReadOnly) { - finishWithError(NOT_ALLOWED, "Calendar with ID $calendarId is read-only", pendingChannelResult) + finishWithError( + EC.NOT_ALLOWED, + "Calendar with ID $calendarId is read-only", + pendingChannelResult + ) return } val eventIdNumber = eventId.toLongOrNull() if (eventIdNumber == null) { - finishWithError(INVALID_ARGUMENT, EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE, pendingChannelResult) + finishWithError( + EC.INVALID_ARGUMENT, + EM.EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE, + pendingChannelResult + ) return } @@ -597,15 +760,25 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { finishWithSuccess(deleteSucceeded > 0, pendingChannelResult) } else { if (!followingInstances!!) { // Only this instance - val exceptionUriWithId = ContentUris.withAppendedId(Events.CONTENT_EXCEPTION_URI, eventIdNumber) + val exceptionUriWithId = + ContentUris.withAppendedId(Events.CONTENT_EXCEPTION_URI, eventIdNumber) val values = ContentValues() - val instanceCursor = CalendarContract.Instances.query(contentResolver, EVENT_INSTANCE_DELETION, startDate!!, endDate!!) + val instanceCursor = CalendarContract.Instances.query( + contentResolver, + Cst.EVENT_INSTANCE_DELETION, + startDate!!, + endDate!! + ) while (instanceCursor.moveToNext()) { - val foundEventID = instanceCursor.getLong(EVENT_INSTANCE_DELETION_ID_INDEX) + val foundEventID = + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX) if (eventIdNumber == foundEventID) { - values.put(Events.ORIGINAL_INSTANCE_TIME, instanceCursor.getLong(EVENT_INSTANCE_DELETION_BEGIN_INDEX)) + values.put( + Events.ORIGINAL_INSTANCE_TIME, + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_BEGIN_INDEX) + ) values.put(Events.STATUS, Events.STATUS_CANCELED) } } @@ -614,32 +787,52 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { instanceCursor.close() finishWithSuccess(deleteSucceeded != null, pendingChannelResult) } else { // This and following instances - val eventsUriWithId = ContentUris.withAppendedId(Events.CONTENT_URI, eventIdNumber) + val eventsUriWithId = + ContentUris.withAppendedId(Events.CONTENT_URI, eventIdNumber) val values = ContentValues() - val instanceCursor = CalendarContract.Instances.query(contentResolver, EVENT_INSTANCE_DELETION, startDate!!, endDate!!) + val instanceCursor = CalendarContract.Instances.query( + contentResolver, + Cst.EVENT_INSTANCE_DELETION, + startDate!!, + endDate!! + ) while (instanceCursor.moveToNext()) { - val foundEventID = instanceCursor.getLong(EVENT_INSTANCE_DELETION_ID_INDEX) + val foundEventID = + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX) if (eventIdNumber == foundEventID) { - val newRule = org.dmfs.rfc5545.recur.RecurrenceRule(instanceCursor.getString(EVENT_INSTANCE_DELETION_RRULE_INDEX)) - val lastDate = instanceCursor.getLong(EVENT_INSTANCE_DELETION_LAST_DATE_INDEX) + val newRule = + Rrule(instanceCursor.getString(Cst.EVENT_INSTANCE_DELETION_RRULE_INDEX)) + val lastDate = + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_LAST_DATE_INDEX) if (lastDate > 0 && newRule.count != null && newRule.count > 0) { // Update occurrence rule - val cursor = CalendarContract.Instances.query(contentResolver, EVENT_INSTANCE_DELETION, startDate, lastDate) + val cursor = CalendarContract.Instances.query( + contentResolver, + Cst.EVENT_INSTANCE_DELETION, + startDate, + lastDate + ) while (cursor.moveToNext()) { - if (eventIdNumber == cursor.getLong(EVENT_INSTANCE_DELETION_ID_INDEX)) { + if (eventIdNumber == cursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX)) { newRule.count-- } } cursor.close() } else { // Indefinite and specified date rule - val cursor = CalendarContract.Instances.query(contentResolver, EVENT_INSTANCE_DELETION, startDate - DateUtils.YEAR_IN_MILLIS, startDate - 1) + val cursor = CalendarContract.Instances.query( + contentResolver, + Cst.EVENT_INSTANCE_DELETION, + startDate - DateUtils.YEAR_IN_MILLIS, + startDate - 1 + ) var lastRecurrenceDate: Long? = null while (cursor.moveToNext()) { - if (eventIdNumber == cursor.getLong(EVENT_INSTANCE_DELETION_ID_INDEX)) { - lastRecurrenceDate = cursor.getLong(EVENT_INSTANCE_DELETION_END_INDEX) + if (eventIdNumber == cursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX)) { + lastRecurrenceDate = + cursor.getLong(Cst.EVENT_INSTANCE_DELETION_END_INDEX) } } @@ -660,7 +853,11 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } } } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, DELETE_EVENT_REQUEST_CODE, calendarId) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + DELETE_EVENT_REQUEST_CODE, + calendarId + ) parameters.eventId = eventId requestPermissions(parameters) } @@ -683,7 +880,12 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { private fun requestPermissions(requestCode: Int) { if (atLeastAPI(23)) { - _binding!!.activity.requestPermissions(arrayOf(Manifest.permission.WRITE_CALENDAR, Manifest.permission.READ_CALENDAR), requestCode) + _binding!!.activity.requestPermissions( + arrayOf( + Manifest.permission.WRITE_CALENDAR, + Manifest.permission.READ_CALENDAR + ), requestCode + ) } } @@ -692,13 +894,13 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { return null } - val calId = cursor.getLong(CALENDAR_PROJECTION_ID_INDEX) - val displayName = cursor.getString(CALENDAR_PROJECTION_DISPLAY_NAME_INDEX) - val accessLevel = cursor.getInt(CALENDAR_PROJECTION_ACCESS_LEVEL_INDEX) - val calendarColor = cursor.getInt(CALENDAR_PROJECTION_COLOR_INDEX) - val accountName = cursor.getString(CALENDAR_PROJECTION_ACCOUNT_NAME_INDEX) - val accountType = cursor.getString(CALENDAR_PROJECTION_ACCOUNT_TYPE_INDEX) - val ownerAccount = cursor.getString(CALENDAR_PROJECTION_OWNER_ACCOUNT_INDEX) + val calId = cursor.getLong(Cst.CALENDAR_PROJECTION_ID_INDEX) + val displayName = cursor.getString(Cst.CALENDAR_PROJECTION_DISPLAY_NAME_INDEX) + val accessLevel = cursor.getInt(Cst.CALENDAR_PROJECTION_ACCESS_LEVEL_INDEX) + val calendarColor = cursor.getInt(Cst.CALENDAR_PROJECTION_COLOR_INDEX) + val accountName = cursor.getString(Cst.CALENDAR_PROJECTION_ACCOUNT_NAME_INDEX) + val accountType = cursor.getString(Cst.CALENDAR_PROJECTION_ACCOUNT_TYPE_INDEX) + val ownerAccount = cursor.getString(Cst.CALENDAR_PROJECTION_OWNER_ACCOUNT_INDEX) val calendar = Calendar( calId.toString(), @@ -711,7 +913,7 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { calendar.isReadOnly = isCalendarReadOnly(accessLevel) if (atLeastAPI(17)) { - val isPrimary = cursor.getString(CALENDAR_PROJECTION_IS_PRIMARY_INDEX) + val isPrimary = cursor.getString(Cst.CALENDAR_PROJECTION_IS_PRIMARY_INDEX) calendar.isDefault = isPrimary == "1" } else { calendar.isDefault = false @@ -723,21 +925,19 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { if (cursor == null) { return null } - - val eventId = cursor.getLong(EVENT_PROJECTION_ID_INDEX) - val title = cursor.getString(EVENT_PROJECTION_TITLE_INDEX) - val description = cursor.getString(EVENT_PROJECTION_DESCRIPTION_INDEX) - val begin = cursor.getLong(EVENT_PROJECTION_BEGIN_INDEX) - val end = cursor.getLong(EVENT_PROJECTION_END_INDEX) - val recurringRule = cursor.getString(EVENT_PROJECTION_RECURRING_RULE_INDEX) - val allDay = cursor.getInt(EVENT_PROJECTION_ALL_DAY_INDEX) > 0 - val location = cursor.getString(EVENT_PROJECTION_EVENT_LOCATION_INDEX) - val url = cursor.getString(EVENT_PROJECTION_CUSTOM_APP_URI_INDEX) - val startTimeZone = cursor.getString(EVENT_PROJECTION_START_TIMEZONE_INDEX) - val endTimeZone = cursor.getString(EVENT_PROJECTION_END_TIMEZONE_INDEX) - val availability = parseAvailability(cursor.getInt(EVENT_PROJECTION_AVAILABILITY_INDEX)) - val eventStatus = parseEventStatus(cursor.getInt(EVENT_PROJECTION_STATUS_INDEX)) - + val eventId = cursor.getLong(Cst.EVENT_PROJECTION_ID_INDEX) + val title = cursor.getString(Cst.EVENT_PROJECTION_TITLE_INDEX) + val description = cursor.getString(Cst.EVENT_PROJECTION_DESCRIPTION_INDEX) + val begin = cursor.getLong(Cst.EVENT_PROJECTION_BEGIN_INDEX) + val end = cursor.getLong(Cst.EVENT_PROJECTION_END_INDEX) + val recurringRule = cursor.getString(Cst.EVENT_PROJECTION_RECURRING_RULE_INDEX) + val allDay = cursor.getInt(Cst.EVENT_PROJECTION_ALL_DAY_INDEX) > 0 + val location = cursor.getString(Cst.EVENT_PROJECTION_EVENT_LOCATION_INDEX) + val url = cursor.getString(Cst.EVENT_PROJECTION_CUSTOM_APP_URI_INDEX) + val startTimeZone = cursor.getString(Cst.EVENT_PROJECTION_START_TIMEZONE_INDEX) + 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 event = Event() event.eventTitle = title ?: "New Event" event.eventId = eventId.toString() @@ -761,64 +961,79 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { if (recurrenceRuleString == null) { return null } - - val rfcRecurrenceRule = org.dmfs.rfc5545.recur.RecurrenceRule(recurrenceRuleString) + val rfcRecurrenceRule = Rrule(recurrenceRuleString) val frequency = when (rfcRecurrenceRule.freq) { - Freq.YEARLY -> RecurrenceFrequency.YEARLY - Freq.MONTHLY -> RecurrenceFrequency.MONTHLY - Freq.WEEKLY -> RecurrenceFrequency.WEEKLY - Freq.DAILY -> RecurrenceFrequency.DAILY + RruleFreq.YEARLY -> RruleFreq.YEARLY + RruleFreq.MONTHLY -> RruleFreq.MONTHLY + RruleFreq.WEEKLY -> RruleFreq.WEEKLY + RruleFreq.DAILY -> RruleFreq.DAILY else -> null - } + } ?: return null + //Avoid handling HOURLY/MINUTELY/SECONDLY frequencies for now - val recurrenceRule = RecurrenceRule(frequency!!) - if (rfcRecurrenceRule.count != null) { - recurrenceRule.totalOccurrences = rfcRecurrenceRule.count - } + val recurrenceRule = RecurrenceRule(frequency) + recurrenceRule.count = rfcRecurrenceRule.count recurrenceRule.interval = rfcRecurrenceRule.interval - if (rfcRecurrenceRule.until != null) { - recurrenceRule.endDate = rfcRecurrenceRule.until.timestamp + + val until = rfcRecurrenceRule.until + if (until != null) { + recurrenceRule.until = formatDateTime(dateTime = until) } - when (rfcRecurrenceRule.freq) { - Freq.WEEKLY, Freq.MONTHLY, Freq.YEARLY -> { - recurrenceRule.daysOfWeek = rfcRecurrenceRule.byDayPart?.mapNotNull { - DayOfWeek.values().find { dayOfWeek -> dayOfWeek.ordinal == it.weekday.ordinal } - }?.toMutableList() + recurrenceRule.sourceRruleString = recurrenceRuleString + + //TODO: Force set to Monday (atm RRULE package only seem to support Monday) + recurrenceRule.wkst = /*rfcRecurrenceRule.weekStart.name*/Weekday.MO.name + recurrenceRule.byday = rfcRecurrenceRule.byDayPart?.mapNotNull { + it.toString() + }?.toMutableList() + recurrenceRule.bymonthday = rfcRecurrenceRule.getByPart(Rrule.Part.BYMONTHDAY) + recurrenceRule.byyearday = rfcRecurrenceRule.getByPart(Rrule.Part.BYYEARDAY) + recurrenceRule.byweekno = rfcRecurrenceRule.getByPart(Rrule.Part.BYWEEKNO) + + // Below adjustment of byMonth ints is necessary as the library somehow gives a wrong int + // See also [buildRecurrenceRuleParams] where 1 is subtracted. + val oldByMonth = rfcRecurrenceRule.getByPart(Rrule.Part.BYMONTH) + if (oldByMonth != null) { + val newByMonth = mutableListOf() + for (month in oldByMonth) { + newByMonth.add(month + 1) } + recurrenceRule.bymonth = newByMonth + } else { + recurrenceRule.bymonth = rfcRecurrenceRule.getByPart(Rrule.Part.BYMONTH) } - val rfcRecurrenceRuleString = rfcRecurrenceRule.toString() - if (rfcRecurrenceRule.freq == Freq.MONTHLY || rfcRecurrenceRule.freq == Freq.YEARLY) { - // Get week number value from BYSETPOS - recurrenceRule.weekOfMonth = convertCalendarPartToNumericValues(rfcRecurrenceRuleString, BYSETPOS_PART) + recurrenceRule.bysetpos = rfcRecurrenceRule.getByPart(Rrule.Part.BYSETPOS) - // If value is not found in BYSETPOS and not repeating by nth day or nth month - // Get the week number value from the BYDAY position - if (recurrenceRule.weekOfMonth == null && rfcRecurrenceRule.byDayPart != null) { - recurrenceRule.weekOfMonth = rfcRecurrenceRule.byDayPart.first().pos - } + return recurrenceRule + } - recurrenceRule.dayOfMonth = convertCalendarPartToNumericValues(rfcRecurrenceRuleString, BYMONTHDAY_PART) + private fun formatDateTime(dateTime: DateTime): String { + assert(dateTime.year in 0..9999) - if (rfcRecurrenceRule.freq == Freq.YEARLY) { - recurrenceRule.monthOfYear = convertCalendarPartToNumericValues(rfcRecurrenceRuleString, BYMONTH_PART) - } + fun twoDigits(n: Int): String { + return if (n < 10) "0$n" else "$n" } - return recurrenceRule - } - - private fun convertCalendarPartToNumericValues(rfcRecurrenceRuleString: String, partName: String): Int? { - val partIndex = rfcRecurrenceRuleString.indexOf(partName) - if (partIndex == -1) { - return null + fun fourDigits(n: Int): String { + val absolute = n.absoluteValue + val sign = if (n < 0) "-" else "" + if (absolute >= 1000) return "$n" + if (absolute >= 100) return "${sign}0$absolute" + if (absolute >= 10) return "${sign}00$absolute" + return "${sign}000$absolute" } - return rfcRecurrenceRuleString.substring(partIndex).split(";").firstOrNull()?.split("=")?.lastOrNull()?.split(",")?.map { - it.toInt() - }?.firstOrNull() + val year = fourDigits(dateTime.year) + val month = twoDigits(dateTime.month.plus(1)) + val day = twoDigits(dateTime.dayOfMonth) + val hour = twoDigits(dateTime.hours) + val minute = twoDigits(dateTime.minutes) + val second = twoDigits(dateTime.seconds) + val utcSuffix = if (dateTime.timeZone == UTC) 'Z' else "" + return "$year-$month-${day}T$hour:$minute:$second$utcSuffix" } private fun parseAttendeeRow(calendar: Calendar, cursor: Cursor?): Attendee? { @@ -826,14 +1041,14 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { return null } - val emailAddress = cursor.getString(ATTENDEE_EMAIL_INDEX) + val emailAddress = cursor.getString(Cst.ATTENDEE_EMAIL_INDEX) return Attendee( emailAddress, - cursor.getString(ATTENDEE_NAME_INDEX), - cursor.getInt(ATTENDEE_TYPE_INDEX), - cursor.getInt(ATTENDEE_STATUS_INDEX), - cursor.getInt(ATTENDEE_RELATIONSHIP_INDEX) == CalendarContract.Attendees.RELATIONSHIP_ORGANIZER, + cursor.getString(Cst.ATTENDEE_NAME_INDEX), + cursor.getInt(Cst.ATTENDEE_TYPE_INDEX), + cursor.getInt(Cst.ATTENDEE_STATUS_INDEX), + cursor.getInt(Cst.ATTENDEE_RELATIONSHIP_INDEX) == CalendarContract.Attendees.RELATIONSHIP_ORGANIZER, emailAddress == calendar.ownerAccount ) } @@ -843,7 +1058,7 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { return null } - return Reminder(cursor.getInt(REMINDER_MINUTES_INDEX)) + return Reminder(cursor.getInt(Cst.REMINDER_MINUTES_INDEX)) } private fun isCalendarReadOnly(accessLevel: Int): Boolean { @@ -858,10 +1073,20 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } @SuppressLint("MissingPermission") - private fun retrieveAttendees(calendar: Calendar, eventId: String, contentResolver: ContentResolver?): MutableList { + private fun retrieveAttendees( + calendar: Calendar, + eventId: String, + contentResolver: ContentResolver? + ): MutableList { val attendees: MutableList = mutableListOf() val attendeesQuery = "(${CalendarContract.Attendees.EVENT_ID} = ${eventId})" - val attendeesCursor = contentResolver?.query(CalendarContract.Attendees.CONTENT_URI, ATTENDEE_PROJECTION, attendeesQuery, null, null) + val attendeesCursor = contentResolver?.query( + CalendarContract.Attendees.CONTENT_URI, + Cst.ATTENDEE_PROJECTION, + attendeesQuery, + null, + null + ) attendeesCursor.use { cursor -> if (cursor?.moveToFirst() == true) { do { @@ -875,10 +1100,19 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } @SuppressLint("MissingPermission") - private fun retrieveReminders(eventId: String, contentResolver: ContentResolver?): MutableList { + private fun retrieveReminders( + eventId: String, + contentResolver: ContentResolver? + ): MutableList { val reminders: MutableList = mutableListOf() val remindersQuery = "(${CalendarContract.Reminders.EVENT_ID} = ${eventId})" - val remindersCursor = contentResolver?.query(CalendarContract.Reminders.CONTENT_URI, REMINDER_PROJECTION, remindersQuery, null, null) + val remindersCursor = contentResolver?.query( + CalendarContract.Reminders.CONTENT_URI, + Cst.REMINDER_PROJECTION, + remindersQuery, + null, + null + ) remindersCursor.use { cursor -> if (cursor?.moveToFirst() == true) { do { @@ -906,13 +1140,19 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { clearCachedParameters(pendingChannelResult) } - private fun finishWithError(errorCode: String, errorMessage: String?, pendingChannelResult: MethodChannel.Result) { + private fun finishWithError( + errorCode: String, + errorMessage: String?, + pendingChannelResult: MethodChannel.Result + ) { pendingChannelResult.error(errorCode, errorMessage, null) clearCachedParameters(pendingChannelResult) } private fun clearCachedParameters(pendingChannelResult: MethodChannel.Result) { - val cachedParameters = _cachedParametersMap.values.filter { it.pendingChannelResult == pendingChannelResult }.toList() + val cachedParameters = + _cachedParametersMap.values.filter { it.pendingChannelResult == pendingChannelResult } + .toList() for (cachedParameter in cachedParameters) { if (_cachedParametersMap.containsKey(cachedParameter.ownCacheKey)) { _cachedParametersMap.remove(cachedParameter.ownCacheKey) @@ -921,71 +1161,92 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } private fun atLeastAPI(api: Int): Boolean { - return api <= android.os.Build.VERSION.SDK_INT + return api <= Build.VERSION.SDK_INT } - private fun buildRecurrenceRuleParams(recurrenceRule: RecurrenceRule): String { - val frequencyParam = when (recurrenceRule.recurrenceFrequency) { - RecurrenceFrequency.DAILY -> Freq.DAILY - RecurrenceFrequency.WEEKLY -> Freq.WEEKLY - RecurrenceFrequency.MONTHLY -> Freq.MONTHLY - RecurrenceFrequency.YEARLY -> Freq.YEARLY - } - val rr = org.dmfs.rfc5545.recur.RecurrenceRule(frequencyParam) + private fun buildRecurrenceRuleParams(recurrenceRule: RecurrenceRule): String? { + val frequencyParam = when (recurrenceRule.freq) { + RruleFreq.DAILY -> RruleFreq.DAILY + RruleFreq.WEEKLY -> RruleFreq.WEEKLY + RruleFreq.MONTHLY -> RruleFreq.MONTHLY + RruleFreq.YEARLY -> RruleFreq.YEARLY + else -> null + } ?: return null + + val rr = Rrule(frequencyParam) if (recurrenceRule.interval != null) { rr.interval = recurrenceRule.interval!! } - if (recurrenceRule.recurrenceFrequency == RecurrenceFrequency.WEEKLY || - recurrenceRule.weekOfMonth != null && (recurrenceRule.recurrenceFrequency == RecurrenceFrequency.MONTHLY || recurrenceRule.recurrenceFrequency == RecurrenceFrequency.YEARLY)) { - rr.byDayPart = buildByDayPart(recurrenceRule) + if (recurrenceRule.count != null) { + rr.count = recurrenceRule.count!! + } else if (recurrenceRule.until != null) { + var untilString: String = recurrenceRule.until!! + if (!untilString.endsWith("Z")) { + untilString += "Z" + } + rr.until = parseDateTime(untilString) } - if (recurrenceRule.totalOccurrences != null) { - rr.count = recurrenceRule.totalOccurrences!! - } else if (recurrenceRule.endDate != null) { - val calendar = java.util.Calendar.getInstance() - calendar.timeInMillis = recurrenceRule.endDate!! - val dateFormat = SimpleDateFormat("yyyyMMdd") - dateFormat.timeZone = calendar.timeZone - rr.until = DateTime(calendar.timeZone, recurrenceRule.endDate!!) + if (recurrenceRule.wkst != null) { + rr.weekStart = Weekday.valueOf(recurrenceRule.wkst!!) } - var rrString = rr.toString() - - if (recurrenceRule.monthOfYear != null && recurrenceRule.recurrenceFrequency == RecurrenceFrequency.YEARLY) { - rrString = rrString.addPartWithValues(BYMONTH_PART, recurrenceRule.monthOfYear) + if (recurrenceRule.byday != null) { + rr.byDayPart = recurrenceRule.byday?.mapNotNull { + WeekdayNum.valueOf(it) + }?.toMutableList() } - if (recurrenceRule.recurrenceFrequency == RecurrenceFrequency.MONTHLY || recurrenceRule.recurrenceFrequency == RecurrenceFrequency.YEARLY) { - if (recurrenceRule.weekOfMonth == null) { - rrString = rrString.addPartWithValues(BYMONTHDAY_PART, recurrenceRule.dayOfMonth) - } + if (recurrenceRule.bymonthday != null) { + rr.setByPart(Rrule.Part.BYMONTHDAY, recurrenceRule.bymonthday!!) } - return rrString - } - - private fun buildByDayPart(recurrenceRule: RecurrenceRule): List? { - if (recurrenceRule.daysOfWeek?.isEmpty() == true) { - return null + if (recurrenceRule.byyearday != null) { + rr.setByPart(Rrule.Part.BYYEARDAY, recurrenceRule.byyearday!!) } - return recurrenceRule.daysOfWeek?.mapNotNull { dayOfWeek -> - Weekday.values().firstOrNull { - it.ordinal == dayOfWeek.ordinal + if (recurrenceRule.byweekno != null) { + rr.setByPart(Rrule.Part.BYWEEKNO, recurrenceRule.byweekno!!) + } + // Below adjustment of byMonth ints is necessary as the library somehow gives a wrong int + // See also [parseRecurrenceRuleString] where +1 is added. + if (recurrenceRule.bymonth != null) { + val byMonth = recurrenceRule.bymonth!! + val newMonth = mutableListOf() + byMonth.forEach { + newMonth.add(it - 1) } - }?.map { - org.dmfs.rfc5545.recur.RecurrenceRule.WeekdayNum(recurrenceRule.weekOfMonth ?: 0, it) + rr.setByPart(Rrule.Part.BYMONTH, newMonth) } - } - private fun String.addPartWithValues(partName: String, values: Int?): String { - if (values != null) { - return this + PART_TEMPLATE.format(partName) + values + if (recurrenceRule.bysetpos != null) { + rr.setByPart(Rrule.Part.BYSETPOS, recurrenceRule.bysetpos!!) } + return rr.toString() + } - return this + private fun parseDateTime(string: String): DateTime { + val year = Regex("""(?\d{4})""").pattern + val month = Regex("""(?\d{2})""").pattern + val day = Regex("""(?\d{2})""").pattern + val hour = Regex("""(?\d{2})""").pattern + val minute = Regex("""(?\d{2})""").pattern + val second = Regex("""(?\d{2})""").pattern + + val regEx = Regex("^$year-$month-${day}T$hour:$minute:${second}Z?\$") + + val match = regEx.matchEntire(string) + + return DateTime( + UTC, + match?.groups?.get(1)?.value?.toIntOrNull() ?: 0, + match?.groups?.get(2)?.value?.toIntOrNull()?.minus(1) ?: 0, + match?.groups?.get(3)?.value?.toIntOrNull() ?: 0, + match?.groups?.get(4)?.value?.toIntOrNull() ?: 0, + match?.groups?.get(5)?.value?.toIntOrNull() ?: 0, + match?.groups?.get(6)?.value?.toIntOrNull() ?: 0 + ) } private fun parseAvailability(availability: Int): Availability? = when (availability) { diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/DayOfWeekSerializer.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/DayOfWeekSerializer.kt deleted file mode 100644 index b2bbce05..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/DayOfWeekSerializer.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.builttoroam.devicecalendar - -import com.builttoroam.devicecalendar.common.DayOfWeek -import com.google.gson.* -import java.lang.reflect.Type - -class DayOfWeekSerializer: JsonSerializer { - override fun serialize(src: DayOfWeek?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { - if(src != null) { - return JsonPrimitive(src.ordinal) - } - return JsonObject() - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt index d2aef019..c1f14533 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt @@ -4,8 +4,6 @@ import android.app.Activity import android.content.Context import androidx.annotation.NonNull import com.builttoroam.devicecalendar.common.Constants -import com.builttoroam.devicecalendar.common.DayOfWeek -import com.builttoroam.devicecalendar.common.RecurrenceFrequency import com.builttoroam.devicecalendar.models.* import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware @@ -13,11 +11,63 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result +import org.dmfs.rfc5545.recur.Freq const val CHANNEL_NAME = "plugins.builttoroam.com/device_calendar" -class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { +// Methods +private const val REQUEST_PERMISSIONS_METHOD = "requestPermissions" +private const val HAS_PERMISSIONS_METHOD = "hasPermissions" +private const val RETRIEVE_CALENDARS_METHOD = "retrieveCalendars" +private const val RETRIEVE_EVENTS_METHOD = "retrieveEvents" +private const val DELETE_EVENT_METHOD = "deleteEvent" +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" + +// Method arguments +private const val CALENDAR_ID_ARGUMENT = "calendarId" +private const val CALENDAR_NAME_ARGUMENT = "calendarName" +private const val START_DATE_ARGUMENT = "startDate" +private const val END_DATE_ARGUMENT = "endDate" +private const val EVENT_IDS_ARGUMENT = "eventIds" +private const val EVENT_ID_ARGUMENT = "eventId" +private const val EVENT_TITLE_ARGUMENT = "eventTitle" +private const val EVENT_LOCATION_ARGUMENT = "eventLocation" +private const val EVENT_URL_ARGUMENT = "eventURL" +private const val EVENT_DESCRIPTION_ARGUMENT = "eventDescription" +private const val EVENT_ALL_DAY_ARGUMENT = "eventAllDay" +private const val EVENT_START_DATE_ARGUMENT = "eventStartDate" +private const val EVENT_END_DATE_ARGUMENT = "eventEndDate" +private const val EVENT_START_TIMEZONE_ARGUMENT = "eventStartTimeZone" +private const val EVENT_END_TIMEZONE_ARGUMENT = "eventEndTimeZone" +private const val RECURRENCE_RULE_ARGUMENT = "recurrenceRule" +private const val FREQUENCY_ARGUMENT = "freq" +private const val COUNT_ARGUMENT = "count" +private const val UNTIL_ARGUMENT = "until" +private const val INTERVAL_ARGUMENT = "interval" +private const val BY_WEEK_DAYS_ARGUMENT = "byday" +private const val BY_MONTH_DAYS_ARGUMENT = "bymonthday" +private const val BY_YEAR_DAYS_ARGUMENT = "byyearday" +private const val BY_WEEKS_ARGUMENT = "byweekno" +private const val BY_MONTH_ARGUMENT = "bymonth" +private const val BY_SET_POSITION_ARGUMENT = "bysetpos" + +private const val ATTENDEES_ARGUMENT = "attendees" +private const val EMAIL_ADDRESS_ARGUMENT = "emailAddress" +private const val NAME_ARGUMENT = "name" +private const val ROLE_ARGUMENT = "role" +private const val REMINDERS_ARGUMENT = "reminders" +private const val MINUTES_ARGUMENT = "minutes" +private const val FOLLOWING_INSTANCES = "followingInstances" +private const val CALENDAR_COLOR_ARGUMENT = "calendarColor" +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" + +class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { /// The MethodChannel that will the communication between Flutter and native Android /// @@ -27,54 +77,6 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { private var context: Context? = null private var activity: Activity? = null - // Methods - private val REQUEST_PERMISSIONS_METHOD = "requestPermissions" - private val HAS_PERMISSIONS_METHOD = "hasPermissions" - private val RETRIEVE_CALENDARS_METHOD = "retrieveCalendars" - private val RETRIEVE_EVENTS_METHOD = "retrieveEvents" - private val DELETE_EVENT_METHOD = "deleteEvent" - private val DELETE_EVENT_INSTANCE_METHOD = "deleteEventInstance" - private val CREATE_OR_UPDATE_EVENT_METHOD = "createOrUpdateEvent" - private val CREATE_CALENDAR_METHOD = "createCalendar" - private val DELETE_CALENDAR_METHOD = "deleteCalendar" - - // Method arguments - private val CALENDAR_ID_ARGUMENT = "calendarId" - private val CALENDAR_NAME_ARGUMENT = "calendarName" - private val START_DATE_ARGUMENT = "startDate" - private val END_DATE_ARGUMENT = "endDate" - private val EVENT_IDS_ARGUMENT = "eventIds" - private val EVENT_ID_ARGUMENT = "eventId" - private val EVENT_TITLE_ARGUMENT = "eventTitle" - private val EVENT_LOCATION_ARGUMENT = "eventLocation" - private val EVENT_URL_ARGUMENT = "eventURL" - private val EVENT_DESCRIPTION_ARGUMENT = "eventDescription" - private val EVENT_ALL_DAY_ARGUMENT = "eventAllDay" - private val EVENT_START_DATE_ARGUMENT = "eventStartDate" - private val EVENT_END_DATE_ARGUMENT = "eventEndDate" - private val EVENT_START_TIMEZONE_ARGUMENT = "eventStartTimeZone" - private val EVENT_END_TIMEZONE_ARGUMENT = "eventEndTimeZone" - private val RECURRENCE_RULE_ARGUMENT = "recurrenceRule" - private val RECURRENCE_FREQUENCY_ARGUMENT = "recurrenceFrequency" - private val TOTAL_OCCURRENCES_ARGUMENT = "totalOccurrences" - private val INTERVAL_ARGUMENT = "interval" - private val DAYS_OF_WEEK_ARGUMENT = "daysOfWeek" - private val DAY_OF_MONTH_ARGUMENT = "dayOfMonth" - private val MONTH_OF_YEAR_ARGUMENT = "monthOfYear" - private val WEEK_OF_MONTH_ARGUMENT = "weekOfMonth" - private val ATTENDEES_ARGUMENT = "attendees" - private val EMAIL_ADDRESS_ARGUMENT = "emailAddress" - private val NAME_ARGUMENT = "name" - private val ROLE_ARGUMENT = "role" - private val REMINDERS_ARGUMENT = "reminders" - private val MINUTES_ARGUMENT = "minutes" - private val FOLLOWING_INSTANCES = "followingInstances" - private val CALENDAR_COLOR_ARGUMENT = "calendarColor" - private val LOCAL_ACCOUNT_NAME_ARGUMENT = "localAccountName" - private val EVENT_AVAILABILITY_ARGUMENT = "availability" - private val ATTENDANCE_STATUS_ARGUMENT = "attendanceStatus" - private val EVENT_STATUS_ARGUMENT = "eventStatus" - private lateinit var _calendarDelegate: CalendarDelegate override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { @@ -108,7 +110,7 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { activity = null } - override fun onMethodCall(call: MethodCall, result: Result) { + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { REQUEST_PERMISSIONS_METHOD -> { _calendarDelegate.requestPermissions(result) @@ -124,13 +126,11 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { val startDate = call.argument(START_DATE_ARGUMENT) val endDate = call.argument(END_DATE_ARGUMENT) val eventIds = call.argument>(EVENT_IDS_ARGUMENT) ?: listOf() - _calendarDelegate.retrieveEvents(calendarId!!, startDate, endDate, eventIds, result) } CREATE_OR_UPDATE_EVENT_METHOD -> { val calendarId = call.argument(CALENDAR_ID_ARGUMENT) val event = parseEventArgs(call, calendarId) - _calendarDelegate.createOrUpdateEvent(calendarId!!, event, result) } DELETE_EVENT_METHOD -> { @@ -146,18 +146,30 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { val endDate = call.argument(EVENT_END_DATE_ARGUMENT) val followingInstances = call.argument(FOLLOWING_INSTANCES) - _calendarDelegate.deleteEvent(calendarId!!, eventId!!, result, startDate, endDate, followingInstances) + _calendarDelegate.deleteEvent( + calendarId!!, + eventId!!, + result, + startDate, + endDate, + followingInstances + ) } CREATE_CALENDAR_METHOD -> { val calendarName = call.argument(CALENDAR_NAME_ARGUMENT) val calendarColor = call.argument(CALENDAR_COLOR_ARGUMENT) val localAccountName = call.argument(LOCAL_ACCOUNT_NAME_ARGUMENT) - _calendarDelegate.createCalendar(calendarName!!, calendarColor, localAccountName!!, result) + _calendarDelegate.createCalendar( + calendarName!!, + calendarColor, + localAccountName!!, + result + ) } DELETE_CALENDAR_METHOD -> { val calendarId = call.argument(CALENDAR_ID_ARGUMENT) - _calendarDelegate.deleteCalendar(calendarId!!,result) + _calendarDelegate.deleteCalendar(calendarId!!, result) } else -> { result.notImplemented() @@ -181,67 +193,91 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { event.availability = parseAvailability(call.argument(EVENT_AVAILABILITY_ARGUMENT)) event.eventStatus = parseEventStatus(call.argument(EVENT_STATUS_ARGUMENT)) - if (call.hasArgument(RECURRENCE_RULE_ARGUMENT) && call.argument>(RECURRENCE_RULE_ARGUMENT) != null) { + if (call.hasArgument(RECURRENCE_RULE_ARGUMENT) && call.argument>( + RECURRENCE_RULE_ARGUMENT + ) != null + ) { val recurrenceRule = parseRecurrenceRuleArgs(call) event.recurrenceRule = recurrenceRule } - if (call.hasArgument(ATTENDEES_ARGUMENT) && call.argument>>(ATTENDEES_ARGUMENT) != null) { + if (call.hasArgument(ATTENDEES_ARGUMENT) && call.argument>>( + ATTENDEES_ARGUMENT + ) != null + ) { event.attendees = mutableListOf() val attendeesArgs = call.argument>>(ATTENDEES_ARGUMENT)!! for (attendeeArgs in attendeesArgs) { - event.attendees.add(Attendee( + event.attendees.add( + Attendee( attendeeArgs[EMAIL_ADDRESS_ARGUMENT] as String, attendeeArgs[NAME_ARGUMENT] as String?, attendeeArgs[ROLE_ARGUMENT] as Int, attendeeArgs[ATTENDANCE_STATUS_ARGUMENT] as Int?, - null, null)) + null, null + ) + ) } } - if (call.hasArgument(REMINDERS_ARGUMENT) && call.argument>>(REMINDERS_ARGUMENT) != null) { + if (call.hasArgument(REMINDERS_ARGUMENT) && call.argument>>( + REMINDERS_ARGUMENT + ) != null + ) { event.reminders = mutableListOf() val remindersArgs = call.argument>>(REMINDERS_ARGUMENT)!! for (reminderArgs in remindersArgs) { event.reminders.add(Reminder(reminderArgs[MINUTES_ARGUMENT] as Int)) } } - return event } private fun parseRecurrenceRuleArgs(call: MethodCall): RecurrenceRule { val recurrenceRuleArgs = call.argument>(RECURRENCE_RULE_ARGUMENT)!! - val recurrenceFrequencyIndex = recurrenceRuleArgs[RECURRENCE_FREQUENCY_ARGUMENT] as Int - val recurrenceRule = RecurrenceRule(RecurrenceFrequency.values()[recurrenceFrequencyIndex]) - if (recurrenceRuleArgs.containsKey(TOTAL_OCCURRENCES_ARGUMENT)) { - recurrenceRule.totalOccurrences = recurrenceRuleArgs[TOTAL_OCCURRENCES_ARGUMENT] as Int + val recurrenceFrequencyString = recurrenceRuleArgs[FREQUENCY_ARGUMENT] as String + val recurrenceFrequency = Freq.valueOf(recurrenceFrequencyString) + val recurrenceRule = RecurrenceRule(recurrenceFrequency) + + if (recurrenceRuleArgs.containsKey(COUNT_ARGUMENT)) { + recurrenceRule.count = recurrenceRuleArgs[COUNT_ARGUMENT] as Int? } if (recurrenceRuleArgs.containsKey(INTERVAL_ARGUMENT)) { recurrenceRule.interval = recurrenceRuleArgs[INTERVAL_ARGUMENT] as Int } - if (recurrenceRuleArgs.containsKey(END_DATE_ARGUMENT)) { - recurrenceRule.endDate = recurrenceRuleArgs[END_DATE_ARGUMENT] as Long + if (recurrenceRuleArgs.containsKey(UNTIL_ARGUMENT)) { + recurrenceRule.until = recurrenceRuleArgs[UNTIL_ARGUMENT] as String? } - if (recurrenceRuleArgs.containsKey(DAYS_OF_WEEK_ARGUMENT)) { - recurrenceRule.daysOfWeek = recurrenceRuleArgs[DAYS_OF_WEEK_ARGUMENT].toListOf()?.map { DayOfWeek.values()[it] }?.toMutableList() + if (recurrenceRuleArgs.containsKey(BY_WEEK_DAYS_ARGUMENT)) { + recurrenceRule.byday = + recurrenceRuleArgs[BY_WEEK_DAYS_ARGUMENT].toListOf()?.toMutableList() } - if (recurrenceRuleArgs.containsKey(DAY_OF_MONTH_ARGUMENT)) { - recurrenceRule.dayOfMonth = recurrenceRuleArgs[DAY_OF_MONTH_ARGUMENT] as Int + if (recurrenceRuleArgs.containsKey(BY_MONTH_DAYS_ARGUMENT)) { + recurrenceRule.bymonthday = + recurrenceRuleArgs[BY_MONTH_DAYS_ARGUMENT] as MutableList? } - if (recurrenceRuleArgs.containsKey(MONTH_OF_YEAR_ARGUMENT)) { - recurrenceRule.monthOfYear = recurrenceRuleArgs[MONTH_OF_YEAR_ARGUMENT] as Int + if (recurrenceRuleArgs.containsKey(BY_YEAR_DAYS_ARGUMENT)) { + recurrenceRule.byyearday = + recurrenceRuleArgs[BY_YEAR_DAYS_ARGUMENT] as MutableList? } - if (recurrenceRuleArgs.containsKey(WEEK_OF_MONTH_ARGUMENT)) { - recurrenceRule.weekOfMonth = recurrenceRuleArgs[WEEK_OF_MONTH_ARGUMENT] as Int + if (recurrenceRuleArgs.containsKey(BY_WEEKS_ARGUMENT)) { + recurrenceRule.byweekno = recurrenceRuleArgs[BY_WEEKS_ARGUMENT] as MutableList? } + if (recurrenceRuleArgs.containsKey(BY_MONTH_ARGUMENT)) { + recurrenceRule.bymonth = recurrenceRuleArgs[BY_MONTH_ARGUMENT] as MutableList? + } + + if (recurrenceRuleArgs.containsKey(BY_SET_POSITION_ARGUMENT)) { + recurrenceRule.bysetpos = + recurrenceRuleArgs[BY_SET_POSITION_ARGUMENT] as MutableList? + } return recurrenceRule } @@ -249,10 +285,6 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { return (this as List<*>?)?.filterIsInstance()?.toList() } - private inline fun Any?.toMutableListOf(): MutableList? { - return this?.toListOf()?.toMutableList() - } - private fun parseAvailability(value: String?): Availability? = if (value == null || value == Constants.AVAILABILITY_UNAVAILABLE) { null diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/RecurrenceFrequencySerializer.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/RecurrenceFrequencySerializer.kt deleted file mode 100644 index c4374353..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/RecurrenceFrequencySerializer.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.builttoroam.devicecalendar - -import com.builttoroam.devicecalendar.common.RecurrenceFrequency -import com.google.gson.* -import java.lang.reflect.Type - -class RecurrenceFrequencySerializer: JsonSerializer { - override fun serialize(src: RecurrenceFrequency?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { - if(src != null) { - return JsonPrimitive(src.ordinal) - } - return JsonObject() - } - -} \ No newline at end of file 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 95b083a7..9d136ed5 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt @@ -15,26 +15,26 @@ class Constants { // API 17 or higher val CALENDAR_PROJECTION: Array = arrayOf( - CalendarContract.Calendars._ID, // 0 - CalendarContract.Calendars.ACCOUNT_NAME, // 1 - CalendarContract.Calendars.ACCOUNT_TYPE, // 2 - CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, // 3 - CalendarContract.Calendars.OWNER_ACCOUNT, // 4 - CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, // 5 - CalendarContract.Calendars.CALENDAR_COLOR, // 6 - CalendarContract.Calendars.IS_PRIMARY // 7 + CalendarContract.Calendars._ID, // 0 + CalendarContract.Calendars.ACCOUNT_NAME, // 1 + CalendarContract.Calendars.ACCOUNT_TYPE, // 2 + CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, // 3 + CalendarContract.Calendars.OWNER_ACCOUNT, // 4 + CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, // 5 + CalendarContract.Calendars.CALENDAR_COLOR, // 6 + CalendarContract.Calendars.IS_PRIMARY // 7 ) // API 16 or lower val CALENDAR_PROJECTION_OLDER_API: Array = arrayOf( - CalendarContract.Calendars._ID, // 0 - CalendarContract.Calendars.ACCOUNT_NAME, // 1 - CalendarContract.Calendars.ACCOUNT_TYPE, // 2 - CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, // 3 - CalendarContract.Calendars.OWNER_ACCOUNT, // 4 - CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, // 5 - CalendarContract.Calendars.CALENDAR_COLOR // 6 + CalendarContract.Calendars._ID, // 0 + CalendarContract.Calendars.ACCOUNT_NAME, // 1 + CalendarContract.Calendars.ACCOUNT_TYPE, // 2 + CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, // 3 + CalendarContract.Calendars.OWNER_ACCOUNT, // 4 + CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, // 5 + CalendarContract.Calendars.CALENDAR_COLOR // 6 ) const val EVENT_PROJECTION_ID_INDEX: Int = 0 @@ -76,11 +76,11 @@ class Constants { const val EVENT_INSTANCE_DELETION_END_INDEX: Int = 4 val EVENT_INSTANCE_DELETION: Array = arrayOf( - CalendarContract.Instances.EVENT_ID, - CalendarContract.Events.RRULE, - CalendarContract.Events.LAST_DATE, - CalendarContract.Instances.BEGIN, - CalendarContract.Instances.END + CalendarContract.Instances.EVENT_ID, + CalendarContract.Events.RRULE, + CalendarContract.Events.LAST_DATE, + CalendarContract.Instances.BEGIN, + CalendarContract.Instances.END ) const val ATTENDEE_ID_INDEX: Int = 0 @@ -92,19 +92,19 @@ class Constants { const val ATTENDEE_STATUS_INDEX: Int = 6 val ATTENDEE_PROJECTION: Array = arrayOf( - CalendarContract.Attendees._ID, - CalendarContract.Attendees.EVENT_ID, - CalendarContract.Attendees.ATTENDEE_NAME, - CalendarContract.Attendees.ATTENDEE_EMAIL, - CalendarContract.Attendees.ATTENDEE_TYPE, - CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, - CalendarContract.Attendees.ATTENDEE_STATUS + CalendarContract.Attendees._ID, + CalendarContract.Attendees.EVENT_ID, + CalendarContract.Attendees.ATTENDEE_NAME, + CalendarContract.Attendees.ATTENDEE_EMAIL, + CalendarContract.Attendees.ATTENDEE_TYPE, + CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, + CalendarContract.Attendees.ATTENDEE_STATUS ) const val REMINDER_MINUTES_INDEX = 1 val REMINDER_PROJECTION: Array = arrayOf( - CalendarContract.Reminders.EVENT_ID, - CalendarContract.Reminders.MINUTES + CalendarContract.Reminders.EVENT_ID, + CalendarContract.Reminders.MINUTES ) const val AVAILABILITY_UNAVAILABLE = "UNAVAILABLE" diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/DayOfWeek.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/DayOfWeek.kt deleted file mode 100644 index f6c04838..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/DayOfWeek.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.builttoroam.devicecalendar.common - -enum class DayOfWeek { - SUNDAY, - MONDAY, - TUESDAY, - WEDNESDAY, - THURSDAY, - FRIDAY, - SATURDAY -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt index e8a8c82c..e8486baa 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt @@ -2,10 +2,15 @@ package com.builttoroam.devicecalendar.common class ErrorMessages { companion object { - const val CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE: String = "Calendar ID is not a number" - const val EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE: String = "Event ID cannot be null on deletion" - const val RETRIEVE_EVENTS_ARGUMENTS_NOT_VALID_MESSAGE: String = "Provided arguments (i.e. start, end and event ids) are null or empty" - const val CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE: String = "Some of the event arguments are not valid" - const val NOT_AUTHORIZED_MESSAGE: String = "The user has not allowed this application to modify their calendar(s)" + const val CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE: String = + "Calendar ID is not a number" + const val EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE: String = + "Event ID cannot be null on deletion" + const val RETRIEVE_EVENTS_ARGUMENTS_NOT_VALID_MESSAGE: String = + "Provided arguments (i.e. start, end and event ids) are null or empty" + const val CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE: String = + "Some of the event arguments are not valid" + const val NOT_AUTHORIZED_MESSAGE: String = + "The user has not allowed this application to modify their calendar(s)" } } diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/RecurrenceFrequency.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/RecurrenceFrequency.kt deleted file mode 100644 index 5e9551c9..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/RecurrenceFrequency.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.builttoroam.devicecalendar.common - -enum class RecurrenceFrequency { - DAILY, WEEKLY, MONTHLY, YEARLY -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt index 1336d362..825ca964 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt @@ -1,3 +1,10 @@ package com.builttoroam.devicecalendar.models -class Attendee(val emailAddress: String, val name: String?, val role: Int, val attendanceStatus: Int?, val isOrganizer: Boolean?, val isCurrentUser: Boolean?) \ No newline at end of file +class Attendee( + val emailAddress: String, + val name: String?, + val role: Int, + val attendanceStatus: Int?, + val isOrganizer: Boolean?, + val isCurrentUser: Boolean? +) \ No newline at end of file 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..09380c22 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/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt index 955c4768..22bb4c4b 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt @@ -2,13 +2,15 @@ package com.builttoroam.devicecalendar.models import io.flutter.plugin.common.MethodChannel -class CalendarMethodsParametersCacheModel(val pendingChannelResult: MethodChannel.Result, - val calendarDelegateMethodCode: Int, - var calendarId: String = "", - var calendarEventsStartDate: Long? = null, - var calendarEventsEndDate: Long? = null, - var calendarEventsIds: List = listOf(), - var eventId: String = "", - var event: Event? = null) { +class CalendarMethodsParametersCacheModel( + val pendingChannelResult: MethodChannel.Result, + val calendarDelegateMethodCode: Int, + var calendarId: String = "", + var calendarEventsStartDate: Long? = null, + var calendarEventsEndDate: Long? = null, + var calendarEventsIds: List = listOf(), + var eventId: String = "", + var event: Event? = null +) { var ownCacheKey: Int? = null } \ No newline at end of file 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 67152245..456e549f 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt @@ -1,6 +1,5 @@ package com.builttoroam.devicecalendar.models - class Event { var eventTitle: String? = null var eventId: String? = null diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt index 659afe52..1da83111 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt @@ -1,15 +1,17 @@ package com.builttoroam.devicecalendar.models -import com.builttoroam.devicecalendar.common.DayOfWeek -import com.builttoroam.devicecalendar.common.RecurrenceFrequency +import org.dmfs.rfc5545.recur.Freq - -class RecurrenceRule(val recurrenceFrequency : RecurrenceFrequency) { - var totalOccurrences: Int? = null +class RecurrenceRule(val freq: Freq) { + var count: Int? = null var interval: Int? = null - var endDate: Long? = null - var daysOfWeek: MutableList? = null - var dayOfMonth: Int? = null - var monthOfYear: Int? = null - var weekOfMonth: Int? = null + var until: String? = null + var sourceRruleString: String? = null + var wkst: String? = null + var byday: MutableList? = null + var bymonthday: MutableList? = null + var byyearday: MutableList? = null + var byweekno: MutableList? = null + var bymonth: MutableList? = null + var bysetpos: MutableList? = null } diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index d4fcc1ad..88d793de 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1 +1,3 @@ -include: package:pedantic/analysis_options.yaml \ No newline at end of file +include: package:pedantic/analysis_options.yaml +#include: package:flutter_lints/flutter.yaml +# TODO: change to flutter lints (https://pub.dev/packages/flutter_lints) \ No newline at end of file diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 09b26adb..f58f9b39 100755 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -31,4 +31,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: d3740c426905916d1f2ada0ddfce28cc99f7b7af -COCOAPODS: 1.10.1 +COCOAPODS: 1.11.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 345447bf..7d88e2f9 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -166,7 +166,7 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = 5X4222W8C2; + DevelopmentTeam = PG8Q9ZR89L; LastSwiftMigration = 1130; ProvisioningStyle = Automatic; }; @@ -423,7 +423,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 5X4222W8C2; + DEVELOPMENT_TEAM = PG8Q9ZR89L; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -455,7 +455,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 5X4222W8C2; + DEVELOPMENT_TEAM = PG8Q9ZR89L; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/example/lib/presentation/event_item.dart b/example/lib/presentation/event_item.dart index 2d9c182d..bc002fd9 100644 --- a/example/lib/presentation/event_item.dart +++ b/example/lib/presentation/event_item.dart @@ -1,11 +1,12 @@ 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 'package:timezone/timezone.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; @@ -39,7 +40,7 @@ class _EventItemState extends State { @override void initState() { super.initState(); - setCurentLocation(); + WidgetsBinding.instance.addPostFrameCallback((_) => setCurentLocation()); } @override @@ -222,8 +223,7 @@ class _EventItemState extends State { ), Expanded( child: Text( - widget._calendarEvent?.status?.enumToString ?? - '', + widget._calendarEvent?.status?.enumToString ?? '', overflow: TextOverflow.ellipsis, ), ) @@ -316,7 +316,7 @@ class _EventItemState extends State { try { timezone = await FlutterNativeTimezone.getLocalTimezone(); } catch (e) { - print('Could not get the local timezone'); + debugPrint('Could not get the local timezone'); } timezone ??= 'Etc/UTC'; _currentLocation = timeZoneDatabase.locations[timezone]; diff --git a/example/lib/presentation/pages/calendar_add.dart b/example/lib/presentation/pages/calendar_add.dart index abce3788..b54dacfb 100644 --- a/example/lib/presentation/pages/calendar_add.dart +++ b/example/lib/presentation/pages/calendar_add.dart @@ -117,7 +117,6 @@ class _CalendarAddPageState extends State { void showInSnackBar(String value) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(value))); - // _scaffoldKey.currentState?.showSnackBar(SnackBar(content: Text(value))); } } diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 8dcc3cd0..116cbec0 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -4,23 +4,25 @@ 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 } class CalendarEventPage extends StatefulWidget { - late final Calendar _calendar; + final Calendar _calendar; final Event? _event; final RecurringEventDialog? _recurringEventDialog; - CalendarEventPage(this._calendar, [this._event, this._recurringEventDialog]); + const CalendarEventPage(this._calendar, + [this._event, this._recurringEventDialog, Key? key]) + : super(key: key); @override _CalendarEventPageState createState() { @@ -34,9 +36,13 @@ class _CalendarEventPageState extends State { final Calendar _calendar; Event? _event; - late DeviceCalendarPlugin _deviceCalendarPlugin; + late final DeviceCalendarPlugin _deviceCalendarPlugin; final RecurringEventDialog? _recurringEventDialog; + DateTime get nowDate => DateTime.now(); + + // TimeOfDay get nowTime => TimeOfDay(hour: nowDate.hour, minute: nowDate.hour); + TZDateTime? _startDate; TimeOfDay? _startTime; @@ -44,26 +50,18 @@ class _CalendarEventPageState extends State { TimeOfDay? _endTime; AutovalidateMode _autovalidate = AutovalidateMode.disabled; - DayOfWeekGroup? _dayOfWeekGroup = DayOfWeekGroup.None; + DayOfWeekGroup _dayOfWeekGroup = DayOfWeekGroup.None; + + RecurrenceRuleEndType _recurrenceRuleEndType = + RecurrenceRuleEndType.Indefinite; + RecurrenceRule? _rrule; - bool _isRecurringEvent = false; - bool _isByDayOfMonth = false; - RecurrenceRuleEndType? _recurrenceRuleEndType; - int? _totalOccurrences; - int? _interval; - late DateTime _recurrenceEndDate; - RecurrenceFrequency? _recurrenceFrequency = RecurrenceFrequency.Daily; - List _daysOfWeek = []; - int? _dayOfMonth; final List _validDaysOfMonth = []; - MonthOfYear? _monthOfYear; - WeekNumber? _weekOfMonth; - DayOfWeek? _selectedDayOfWeek = DayOfWeek.Monday; + Availability _availability = Availability.Busy; EventStatus? _eventStatus; - - List _attendees = []; - List _reminders = []; + List? _attendees; + List? _reminders; String _timezone = 'Etc/UTC'; _CalendarEventPageState( @@ -75,95 +73,89 @@ class _CalendarEventPageState extends State { try { _timezone = await FlutterNativeTimezone.getLocalTimezone(); } catch (e) { - print('Could not get the local timezone'); + debugPrint('Could not get the local timezone'); } _deviceCalendarPlugin = DeviceCalendarPlugin(); - _attendees = []; - _reminders = []; - _recurrenceRuleEndType = RecurrenceRuleEndType.Indefinite; - - if (_event == null) { - print('calendar_event _timezone ------------------------- $_timezone'); - var currentLocation = timeZoneDatabase.locations[_timezone]; + final event = _event; + if (event == null) { + debugPrint( + 'calendar_event _timezone ------------------------- $_timezone'); + final currentLocation = timeZoneDatabase.locations[_timezone]; if (currentLocation != null) { - _startDate = TZDateTime.now(currentLocation); - _endDate = TZDateTime.now(currentLocation).add(Duration(hours: 1)); + final now = TZDateTime.now(currentLocation); + _startDate = now; + _startTime = TimeOfDay(hour: now.hour, minute: now.minute); + final oneHourLater = now.add(const Duration(hours: 1)); + _endDate = oneHourLater; + _endTime = + TimeOfDay(hour: oneHourLater.hour, minute: oneHourLater.minute); } else { var fallbackLocation = timeZoneDatabase.locations['Etc/UTC']; - _startDate = TZDateTime.now(fallbackLocation!); - _endDate = TZDateTime.now(fallbackLocation).add(Duration(hours: 1)); + final now = TZDateTime.now(fallbackLocation!); + _startDate = now; + _startTime = TimeOfDay(hour: now.hour, minute: now.minute); + final oneHourLater = now.add(const Duration(hours: 1)); + _endDate = oneHourLater; + _endTime = + TimeOfDay(hour: oneHourLater.hour, minute: oneHourLater.minute); } - _event = Event(_calendar.id, start: _startDate, end: _endDate); + _event = Event(_calendar.id, + start: _startDate, end: _endDate, availability: _availability); - print('DeviceCalendarPlugin calendar id is: ${_calendar.id}'); + debugPrint('DeviceCalendarPlugin calendar id is: ${_calendar.id}'); - _recurrenceEndDate = _endDate as DateTime; - _dayOfMonth = 1; - _monthOfYear = MonthOfYear.January; - _weekOfMonth = WeekNumber.First; - _availability = Availability.Busy; _eventStatus = EventStatus.None; } else { - _startDate = _event!.start!; - _endDate = _event!.end!; - _isRecurringEvent = _event!.recurrenceRule != null; - - if (_event!.attendees!.isNotEmpty) { - _attendees.addAll(_event!.attendees! as Iterable); + final start = event.start; + final end = event.end; + if (start != null && end != null) { + _startDate = start; + _startTime = TimeOfDay(hour: start.hour, minute: start.minute); + _endDate = end; + _endTime = TimeOfDay(hour: end.hour, minute: end.minute); } - if (_event!.reminders!.isNotEmpty) { - _reminders.addAll(_event!.reminders!); + final attendees = event.attendees; + if (attendees != null && attendees.isNotEmpty) { + _attendees = []; + _attendees?.addAll(attendees as Iterable); } - if (_isRecurringEvent) { - _interval = _event!.recurrenceRule!.interval!; - _totalOccurrences = _event!.recurrenceRule!.totalOccurrences; - _recurrenceFrequency = _event!.recurrenceRule!.recurrenceFrequency; + final reminders = event.reminders; + if (reminders != null && reminders.isNotEmpty) { + _reminders = []; + _reminders?.addAll(reminders); + } - if (_totalOccurrences != null) { + final rrule = event.recurrenceRule; + if (rrule != null) { + // debugPrint('OLD_RRULE: ${rrule.toString()}'); + _rrule = rrule; + if (rrule.count != null) { _recurrenceRuleEndType = RecurrenceRuleEndType.MaxOccurrences; } - - if (_event!.recurrenceRule!.endDate != null) { + if (rrule.until != null) { _recurrenceRuleEndType = RecurrenceRuleEndType.SpecifiedEndDate; - _recurrenceEndDate = _event!.recurrenceRule!.endDate!; - } - - _isByDayOfMonth = _event?.recurrenceRule?.weekOfMonth == null; - _daysOfWeek = _event?.recurrenceRule?.daysOfWeek ?? []; - _monthOfYear = - _event?.recurrenceRule?.monthOfYear ?? MonthOfYear.January; - _weekOfMonth = _event?.recurrenceRule?.weekOfMonth ?? WeekNumber.First; - _selectedDayOfWeek = - _daysOfWeek.isNotEmpty ? _daysOfWeek.first : DayOfWeek.Monday; - _dayOfMonth = _event?.recurrenceRule?.dayOfMonth ?? 1; - - if (_daysOfWeek.isNotEmpty) { - _updateDaysOfWeekGroup(); } } - _availability = _event!.availability; - _eventStatus = _event!.status; + _availability = event.availability; + _eventStatus = event.status; } - _startTime = TimeOfDay(hour: _startDate!.hour, minute: _startDate!.minute); - _endTime = TimeOfDay(hour: _endDate!.hour, minute: _endDate!.minute); - // Getting days of the current month (or a selected month for the yearly recurrence) as a default - _getValidDaysOfMonth(_recurrenceFrequency); + _getValidDaysOfMonth(_rrule?.frequency); setState(() {}); } void printAttendeeDetails(Attendee attendee) { - print( + debugPrint( 'attendee name: ${attendee.name}, email address: ${attendee.emailAddress}, type: ${attendee.role?.enumToString}'); - print( + debugPrint( 'ios specifics - status: ${attendee.iosAttendeeDetails?.attendanceStatus}, type: ${attendee.iosAttendeeDetails?.attendanceStatus?.enumToString}'); - print( + debugPrint( 'android specifics - status ${attendee.androidAttendeeDetails?.attendanceStatus}, type: ${attendee.androidAttendeeDetails?.attendanceStatus?.enumToString}'); } @@ -178,126 +170,126 @@ 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) + if (Platform.isAndroid) + ListTile( + leading: 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: Text('All Day'), + ), Padding( padding: const EdgeInsets.all(10.0), child: DateTimePicker( @@ -328,150 +320,127 @@ class _CalendarEventPageState extends State { }, ), ), - 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 ((_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); + // 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: 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) => + EventAttendeePage())); + if (result != null) { + if (_attendees == null) { + _attendees = []; + } + 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: 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 ?? false) + ? 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 +448,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,516 +462,597 @@ 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 ?? + false, + 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('current user'))), + Visibility( + visible: _attendees?[index].isOrganiser ?? + false, + 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('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, + ), + ) + ], + ), + ), + ); + }, + ), + 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 ?? false) + 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) { + CheckboxListTile( + value: _rrule != null, + title: const Text('Is recurring'), + onChanged: (isChecked) { + if (isChecked != null) { setState(() { - _recurrenceFrequency = selectedFrequency; - _getValidDaysOfMonth(_recurrenceFrequency); + if (isChecked) { + _rrule = + RecurrenceRule(frequency: Frequency.daily); + } else { + _rrule = null; + } }); - }, - 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), - ], - ), + } + }, ), - 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) ...[ - ListTile( - leading: 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)) ...[ + if (_rrule != null) ...[ ListTile( - leading: Text('Day of the month'), - trailing: DropdownButton( - onChanged: (value) { + leading: const Text('Select a Recurrence Type'), + trailing: DropdownButton( + onChanged: (selectedFrequency) { setState(() { - _dayOfMonth = value; + _onFrequencyChange( + selectedFrequency ?? Frequency.daily); + _getValidDaysOfMonth(selectedFrequency); }); }, - value: _dayOfMonth, - items: _validDaysOfMonth - .map((day) => DropdownMenuItem( - value: day, - child: Text(day.toString()), + value: _rrule?.frequency, + items: [ + // Frequency.secondly, + // Frequency.minutely, + // Frequency.hourly, + Frequency.daily, + Frequency.weekly, + Frequency.monthly, + Frequency.yearly, + ] + .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: '${_rrule?.interval ?? 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); + _rrule = _rrule?.copyWith( + interval: int.tryParse(value)); } }, ), ), - Text(' occurrences'), + _recurrenceFrequencyToIntervalText( + _rrule?.frequency), ], ), ), - if (_recurrenceRuleEndType == - RecurrenceRuleEndType.SpecifiedEndDate) - Padding( - padding: const EdgeInsets.all(10.0), - child: DateTimePicker( - labelText: 'Date', - enableTime: false, - selectedDate: _recurrenceEndDate, - selectDate: (DateTime date) { + if (_rrule?.frequency == Frequency.weekly) ...[ + Column( + children: [ + ...DayOfWeek.values.map((day) { + return CheckboxListTile( + title: Text(day.enumToString), + value: _rrule?.byWeekDays + .contains(ByWeekDayEntry(day.index + 1)), + onChanged: (selected) { + setState(() { + if (selected == true) { + _rrule?.byWeekDays + .add(ByWeekDayEntry(day.index + 1)); + } else { + _rrule?.byWeekDays.remove( + ByWeekDayEntry(day.index + 1)); + } + _updateDaysOfWeekGroup(selectedDay: day); + }); + }, + ); + }), + const Divider(color: Colors.black), + ...DayOfWeekGroup.values.map((group) { + return RadioListTile( + title: Text(group.enumToString), + value: group, + groupValue: _dayOfWeekGroup, + onChanged: (DayOfWeekGroup? selected) { + if (selected != null) { + setState(() { + _dayOfWeekGroup = selected; + _updateDaysOfWeek(); + }); + } + }, + controlAffinity: + ListTileControlAffinity.trailing); + }), + ], + ) + ], + if (_rrule?.frequency == Frequency.monthly || + _rrule?.frequency == Frequency.yearly) ...[ + SwitchListTile( + value: _rrule?.hasByMonthDays ?? false, + onChanged: (value) { setState(() { - _recurrenceEndDate = date; + if (value) { + _rrule = _rrule?.copyWith( + byMonthDays: {1}, byWeekDays: {}); + } else { + _rrule = _rrule?.copyWith( + byMonthDays: {}, + byWeekDays: {ByWeekDayEntry(1, 1)}); + } }); }, + title: const Text('By day of the month'), + ) + ], + if (_rrule?.frequency == Frequency.yearly && + (_rrule?.hasByMonthDays ?? false)) ...[ + ListTile( + leading: const Text('Month of the year'), + trailing: DropdownButton( + onChanged: (value) { + if (value != null) { + setState(() { + _rrule = _rrule + ?.copyWith(byMonths: {value.index + 1}); + _getValidDaysOfMonth(_rrule?.frequency); + }); + } + }, + value: MonthOfYear.values.toList()[ + (_rrule?.hasByMonths ?? false) + ? _rrule!.byMonths.first - 1 + : 0], + items: MonthOfYear.values + .map((month) => DropdownMenuItem( + value: month, + child: Text(month.enumToString), + )) + .toList(), + ), + ), + ], + if ((_rrule?.hasByMonthDays ?? false) && + (_rrule?.frequency == Frequency.monthly || + _rrule?.frequency == Frequency.yearly)) ...[ + ListTile( + leading: const Text('Day of the month'), + trailing: DropdownButton( + onChanged: (value) { + if (value != null) { + setState(() { + _rrule = + _rrule?.copyWith(byMonthDays: {value}); + }); + } + }, + value: (_rrule?.hasByMonthDays ?? false) + ? _rrule!.byMonthDays.first + : 1, + items: _validDaysOfMonth + .map((day) => DropdownMenuItem( + value: day, + child: Text(day.toString()), + )) + .toList(), + ), + ), + ], + if (!(_rrule?.hasByMonthDays ?? false) && + (_rrule?.frequency == Frequency.monthly || + _rrule?.frequency == Frequency.yearly)) ...[ + Padding( + padding: const EdgeInsets.fromLTRB(15, 10, 15, 10), + child: Align( + alignment: Alignment.centerLeft, + child: _recurrenceFrequencyToText( + _rrule?.frequency) + .data != + null + ? Text(_recurrenceFrequencyToText( + _rrule?.frequency) + .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) { + if (value != null) { + final weekDay = + _rrule?.byWeekDays.first.day ?? 1; + setState(() { + _rrule = _rrule?.copyWith( + byWeekDays: { + ByWeekDayEntry( + weekDay, value.index + 1) + }); + }); + } + }, + value: WeekNumber.values.toList()[ + (_rrule?.hasByWeekDays ?? false) + ? _weekNumFromWeekDayOccurence( + _rrule!.byWeekDays) + : 0], + items: WeekNumber.values + .map((weekNum) => DropdownMenuItem( + value: weekNum, + child: Text(weekNum.enumToString), + )) + .toList(), + ), + ), + Flexible( + child: DropdownButton( + onChanged: (value) { + if (value != null) { + final weekNo = _rrule + ?.byWeekDays.first.occurrence ?? + 1; + setState(() { + _rrule = _rrule?.copyWith( + byWeekDays: { + ByWeekDayEntry( + value.index + 1, weekNo) + }); + }); + } + }, + value: (_rrule?.hasByWeekDays ?? false) && + _rrule?.byWeekDays.first + .occurrence != + null + ? DayOfWeek.values[ + _rrule!.byWeekDays.first.day - 1] + : DayOfWeek.values[0], + items: DayOfWeek.values + .map((day) => DropdownMenuItem( + value: day, + child: Text(day.enumToString), + )) + .toList(), + ), + ), + if (_rrule?.frequency == Frequency.yearly) ...[ + const Text('of'), + Flexible( + child: DropdownButton( + onChanged: (value) { + if (value != null) { + setState(() { + _rrule = _rrule?.copyWith( + byMonths: {value.index + 1}); + }); + } + }, + value: MonthOfYear.values.toList()[ + (_rrule?.hasByMonths ?? false) + ? _rrule!.byMonths.first - 1 + : 0], + items: MonthOfYear.values + .map((month) => DropdownMenuItem( + value: month, + child: Text(month.enumToString), + )) + .toList(), + ), + ), + ] + ], + ), + ), + ], + ListTile( + leading: const Text('Event ends'), + trailing: DropdownButton( + onChanged: (value) { + setState(() { + if (value != null) { + _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: '${_rrule?.count ?? 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) { + _rrule = _rrule?.copyWith( + count: int.tryParse(value)); + } + }, + ), + ), + const Text(' occurrences'), + ], + ), + ), + if (_recurrenceRuleEndType == + RecurrenceRuleEndType.SpecifiedEndDate) + Padding( + padding: const EdgeInsets.all(10.0), + child: DateTimePicker( + labelText: 'Date', + enableTime: false, + selectedDate: _rrule?.until ?? DateTime.now(), + selectDate: (DateTime date) { + setState(() { + _rrule = _rrule?.copyWith( + until: DateTime( + date.year, + date.month, + date.day, + _endTime?.hour ?? nowDate.hour, + _endTime?.minute ?? + nowDate.minute) + .toUtc()); + }); + }, + ), + ), + ], + ...[ + // TODO: on iPhone (e.g. 8) this seems neccesary to be able to access UI below the FAB + const SizedBox(height: 75), + ] ], - ], + ), ), - ), - 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( + foregroundColor: Colors.white, + backgroundColor: Colors.red), + onPressed: () async { + bool? result = true; + if (!(_rrule != null)) { + 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.shrink(); + }); + } - 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) { _autovalidate = AutovalidateMode.always; // Start validating on every change. - showInSnackBar('Please fix the errors in red before submitting.'); + showInSnackBar( + context, 'Please fix the errors in red before submitting.'); } else { form?.save(); - if (_isRecurringEvent) { - if (!_isByDayOfMonth && - (_recurrenceFrequency == RecurrenceFrequency.Monthly || - _recurrenceFrequency == RecurrenceFrequency.Yearly)) { - // Setting day of the week parameters for WeekNumber to avoid clashing with the weekly recurrence values - _daysOfWeek.clear(); - if (_selectedDayOfWeek != null) { - _daysOfWeek.add(_selectedDayOfWeek as DayOfWeek); - } - } else { - _weekOfMonth = null; - } - - _event?.recurrenceRule = RecurrenceRule(_recurrenceFrequency, - interval: _interval, - totalOccurrences: (_recurrenceRuleEndType == - RecurrenceRuleEndType.MaxOccurrences) - ? _totalOccurrences - : null, - endDate: _recurrenceRuleEndType == - RecurrenceRuleEndType.SpecifiedEndDate - ? _recurrenceEndDate - : null, - daysOfWeek: _daysOfWeek, - dayOfMonth: _dayOfMonth, - monthOfYear: _monthOfYear, - weekOfMonth: _weekOfMonth); - } - _event?.attendees = _attendees; - _event?.reminders = _reminders; - _event?.availability = _availability; - _event?.status = _eventStatus; - var createEventResult = - await _deviceCalendarPlugin.createOrUpdateEvent(_event); - if (createEventResult?.isSuccess == true) { - Navigator.pop(context, true); - } else { - showInSnackBar(createEventResult?.errors - .map((err) => '[${err.errorCode}] ${err.errorMessage}') - .join(' | ') as String); - } + _adjustStartEnd(); + _event?.recurrenceRule = _rrule; + // debugPrint('FINAL_RRULE: ${_rrule.toString()}'); + } + _event?.attendees = _attendees; + _event?.reminders = _reminders; + _event?.availability = _availability; + _event?.status = _eventStatus; + var createEventResult = + await _deviceCalendarPlugin.createOrUpdateEvent(_event); + if (createEventResult?.isSuccess == true) { + Navigator.pop(context, true); + } else { + showInSnackBar( + context, + createEventResult?.errors + .map((err) => '[${err.errorCode}] ${err.errorMessage}') + .join(' | ') as String); } }, - child: Icon(Icons.check), + child: const Icon(Icons.check), ), ), ); } - Text _recurrenceFrequencyToText(RecurrenceFrequency? recurrenceFrequency) { - switch (recurrenceFrequency) { - case RecurrenceFrequency.Daily: - return Text('Daily'); - case RecurrenceFrequency.Weekly: - return Text('Weekly'); - case RecurrenceFrequency.Monthly: - return Text('Monthly'); - case RecurrenceFrequency.Yearly: - return Text('Yearly'); - default: - return Text(''); + Text _recurrenceFrequencyToText(Frequency? recurrenceFrequency) { + if (recurrenceFrequency == Frequency.daily) { + return const Text('Daily'); + } else if (recurrenceFrequency == Frequency.weekly) { + return const Text('Weekly'); + } else if (recurrenceFrequency == Frequency.monthly) { + return const Text('Monthly'); + } else if (recurrenceFrequency == Frequency.yearly) { + return const Text('Yearly'); + } else { + return const Text(''); } } - Text _recurrenceFrequencyToIntervalText( - RecurrenceFrequency? recurrenceFrequency) { - switch (recurrenceFrequency) { - case RecurrenceFrequency.Daily: - return Text(' Day(s)'); - case RecurrenceFrequency.Weekly: - return Text(' Week(s) on'); - case RecurrenceFrequency.Monthly: - return Text(' Month(s)'); - case RecurrenceFrequency.Yearly: - return Text(' Year(s)'); - default: - return Text(''); + Text _recurrenceFrequencyToIntervalText(Frequency? recurrenceFrequency) { + if (recurrenceFrequency == Frequency.daily) { + return const Text(' Day(s)'); + } else if (recurrenceFrequency == Frequency.weekly) { + return const Text(' Week(s) on'); + } else if (recurrenceFrequency == Frequency.monthly) { + return const Text(' Month(s)'); + } else if (recurrenceFrequency == Frequency.yearly) { + return const Text(' Year(s)'); + } else { + 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(''); } } // Get total days of a month - void _getValidDaysOfMonth(RecurrenceFrequency? frequency) { + void _getValidDaysOfMonth(Frequency? frequency) { _validDaysOfMonth.clear(); var totalDays = 0; // Year frequency: Get total days of the selected month - if (frequency == RecurrenceFrequency.Yearly) { + if (frequency == Frequency.yearly) { totalDays = DateTime(DateTime.now().year, - _monthOfYear?.value != null ? _monthOfYear!.value + 1 : 1, 0) + (_rrule?.hasByMonths ?? false) ? _rrule!.byMonths.first : 1, 0) .day; } else { // Otherwise, get total days of the current month @@ -1012,49 +1066,153 @@ class _CalendarEventPageState extends State { } void _updateDaysOfWeek() { - if (_dayOfWeekGroup == null) return; - var days = _dayOfWeekGroup!.getDays; - switch (_dayOfWeekGroup) { case DayOfWeekGroup.Weekday: + _rrule = _rrule?.copyWith(byWeekDays: { + ByWeekDayEntry(1), + ByWeekDayEntry(2), + ByWeekDayEntry(3), + ByWeekDayEntry(4), + ByWeekDayEntry(5), + }); + break; case DayOfWeekGroup.Weekend: + _rrule = _rrule?.copyWith(byWeekDays: { + ByWeekDayEntry(6), + ByWeekDayEntry(7), + }); + break; case DayOfWeekGroup.AllDays: - _daysOfWeek.clear(); - _daysOfWeek.addAll(days.where((a) => _daysOfWeek.every((b) => a != b))); + _rrule = _rrule?.copyWith(byWeekDays: { + ByWeekDayEntry(1), + ByWeekDayEntry(2), + ByWeekDayEntry(3), + ByWeekDayEntry(4), + ByWeekDayEntry(5), + ByWeekDayEntry(6), + ByWeekDayEntry(7), + }); break; case DayOfWeekGroup.None: - _daysOfWeek.clear(); - break; default: - _daysOfWeek.clear(); + _rrule?.byWeekDays.clear(); + break; } + // () => setState(() => {}); } void _updateDaysOfWeekGroup({DayOfWeek? selectedDay}) { - var deepEquality = const DeepCollectionEquality.unordered().equals; - - // If _daysOfWeek contains nothing - if (_daysOfWeek.isEmpty && _dayOfWeekGroup != DayOfWeekGroup.None) { - _dayOfWeekGroup = DayOfWeekGroup.None; - } - // If _daysOfWeek contains Monday to Friday - else if (deepEquality(_daysOfWeek, DayOfWeekGroup.Weekday.getDays) && - _dayOfWeekGroup != DayOfWeekGroup.Weekday) { - _dayOfWeekGroup = DayOfWeekGroup.Weekday; + final byWeekDays = _rrule?.byWeekDays; + if (byWeekDays != null) { + if (byWeekDays.length == 7 && + byWeekDays.every((p0) => + p0.day == 1 || + p0.day == 2 || + p0.day == 3 || + p0.day == 4 || + p0.day == 5 || + p0.day == 6 || + p0.day == 7)) { + _dayOfWeekGroup = DayOfWeekGroup.AllDays; + } else if (byWeekDays.length == 5 && + byWeekDays.every((p0) => + p0.day == 1 || + p0.day == 2 || + p0.day == 3 || + p0.day == 4 || + p0.day == 5) && + byWeekDays.none((p0) => p0.day == 6 || p0.day == 7)) { + _dayOfWeekGroup = DayOfWeekGroup.Weekday; + } else if (byWeekDays.length == 2 && + byWeekDays.every((p0) => p0.day == 6 || p0.day == 7) && + byWeekDays.none((p0) => + p0.day == 1 || + p0.day == 2 || + p0.day == 3 || + p0.day == 4 || + p0.day == 5)) { + _dayOfWeekGroup = DayOfWeekGroup.Weekend; + } else { + _dayOfWeekGroup = DayOfWeekGroup.None; + } } - // If _daysOfWeek contains Saturday and Sunday - else if (deepEquality(_daysOfWeek, DayOfWeekGroup.Weekend.getDays) && - _dayOfWeekGroup != DayOfWeekGroup.Weekend) { - _dayOfWeekGroup = DayOfWeekGroup.Weekend; + } + + int _weekNumFromWeekDayOccurence(Set weekdays) { + final weekNum = weekdays.first.occurrence; + if (weekNum != null) { + return weekNum - 1; + } else { + return 0; } - // If _daysOfWeek contains all days - else if (deepEquality(_daysOfWeek, DayOfWeekGroup.AllDays.getDays) && - _dayOfWeekGroup != DayOfWeekGroup.AllDays) { - _dayOfWeekGroup = DayOfWeekGroup.AllDays; + } + + void _onFrequencyChange(Frequency freq) { + final rrule = _rrule; + if (rrule != null) { + final hasByWeekDays = rrule.hasByWeekDays; + final hasByMonthDays = rrule.hasByMonthDays; + final hasByMonths = rrule.hasByMonths; + if (freq == Frequency.daily || freq == Frequency.weekly) { + if (hasByWeekDays) { + rrule.byWeekDays.clear(); + } + if (hasByMonths) { + rrule.byMonths.clear(); + } + _rrule = rrule.copyWith(frequency: freq); + } + if (freq == Frequency.monthly) { + if (hasByMonths) { + rrule.byMonths.clear(); + } + if (!hasByWeekDays && !hasByMonthDays) { + _rrule = rrule + .copyWith(frequency: freq, byWeekDays: {ByWeekDayEntry(1, 1)}); + } else { + _rrule = rrule.copyWith(frequency: freq); + } + } + if (freq == Frequency.yearly) { + if (!hasByWeekDays || !hasByMonths) { + _rrule = rrule.copyWith( + frequency: freq, + byWeekDays: {ByWeekDayEntry(1, 1)}, + byMonths: {1}); + } else { + _rrule = rrule.copyWith(frequency: freq); + } + } } - // Otherwise null - else { - _dayOfWeekGroup = null; + } + + /// In order to avoid an event instance to appear outside of the recurrence + /// rrule, the start and end date have to be adjusted to match the first + /// instance. + void _adjustStartEnd() { + final start = _event?.start; + final end = _event?.end; + final rrule = _rrule; + if (start != null && end != null && rrule != null) { + final allDay = _event?.allDay ?? false; + final duration = end.difference(start); + final instances = rrule.getAllInstances( + start: allDay + ? DateTime.utc(start.year, start.month, start.day) + : DateTime(start.year, start.month, start.day, start.hour, + start.minute) + .toUtc(), + before: rrule.count == null && rrule.until == null + ? DateTime(start.year + 2, start.month, start.day, start.hour, + start.minute) + .toUtc() + : null); + if (instances.isNotEmpty) { + var newStart = TZDateTime.from(instances.first, start.location); + var newEnd = newStart.add(duration); + _event?.start = newStart; + _event?.end = newEnd; + } } } @@ -1079,7 +1237,6 @@ class _CalendarEventPageState extends State { if (value.isEmpty) { return 'Name is required.'; } - return null; } @@ -1098,7 +1255,7 @@ class _CalendarEventPageState extends State { .add(Duration(hours: time.hour, minutes: time.minute)); } - void showInSnackBar(String value) { + void showInSnackBar(BuildContext context, String value) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(value))); } } diff --git a/example/lib/presentation/pages/calendar_events.dart b/example/lib/presentation/pages/calendar_events.dart index 0cd6f182..7255029f 100644 --- a/example/lib/presentation/pages/calendar_events.dart +++ b/example/lib/presentation/pages/calendar_events.dart @@ -132,12 +132,13 @@ class _CalendarEventsPageState extends State { Future _retrieveCalendarEvents() async { final startDate = DateTime.now().add(Duration(days: -30)); - final endDate = DateTime.now().add(Duration(days: 30)); + // final endDate = DateTime.now().add(Duration(days: 365 * 2)); + final endDate = DateTime.now().add(Duration(days: 365 * 10)); var calendarEventsResult = await _deviceCalendarPlugin.retrieveEvents( _calendar.id, RetrieveEventsParams(startDate: startDate, endDate: endDate)); setState(() { - _calendarEvents = calendarEventsResult.data as List; + _calendarEvents = calendarEventsResult.data ?? []; _isLoading = false; }); } @@ -169,7 +170,7 @@ class _CalendarEventsPageState extends State { onPressed: () async { var returnValue = await _deviceCalendarPlugin.deleteCalendar(_calendar.id!); - print( + debugPrint( 'returnValue: ${returnValue.data}, ${returnValue.errors}'); Navigator.of(context).pop(); Navigator.of(context).pop(); diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index e28dbc7f..5c21d1d4 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -1,7 +1,7 @@ import 'package:device_calendar/device_calendar.dart'; import 'package:device_calendar_example/presentation/pages/calendar_add.dart'; -import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'calendar_events.dart'; @@ -137,8 +137,8 @@ class _CalendarsPageState extends State { setState(() { _calendars = calendarsResult.data as List; }); - } on PlatformException catch (e) { - print(e); + } on PlatformException catch (e, s) { + debugPrint('RETRIEVE_CALENDARS: $e, $s'); } } diff --git a/example/lib/presentation/pages/event_attendee.dart b/example/lib/presentation/pages/event_attendee.dart index 002ba6eb..de9644a9 100644 --- a/example/lib/presentation/pages/event_attendee.dart +++ b/example/lib/presentation/pages/event_attendee.dart @@ -1,8 +1,8 @@ import 'dart:io'; +import 'package:device_calendar/device_calendar.dart'; import 'package:device_calendar_example/common/app_routes.dart'; import 'package:flutter/material.dart'; -import 'package:device_calendar/device_calendar.dart'; late DeviceCalendarPlugin _deviceCalendarPlugin; @@ -14,7 +14,7 @@ class EventAttendeePage extends StatefulWidget { @override _EventAttendeePageState createState() => - _EventAttendeePageState(attendee, eventId); + _EventAttendeePageState(attendee, eventId ?? ''); } class _EventAttendeePageState extends State { diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c6eeea43..5dad60cd 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -3,66 +3,25 @@ description: Demonstrates how to use the device_calendar plugin. version: 3.2.0 environment: - sdk: '>=2.12.0 <3.0.0' + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - intl: - uuid: - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: + intl: ^0.17.0 + uuid: ^3.0.6 + timezone: ^0.9.0 + flutter_native_timezone: ^2.0.0 + device_calendar: + path: ../ dev_dependencies: integration_test: sdk: flutter flutter_test: sdk: flutter - test: - - device_calendar: - path: ../ + flutter_lints: ^2.0.1 -# For information on the generic Dart part of this file, see the -# following page: https://www.dartlang.org/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.io/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.io/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.io/custom-fonts/#from-packages + uses-material-design: true \ No newline at end of file diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index 59730a93..b39ee5da 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -14,8 +14,17 @@ extension EKParticipant { } } +extension String { + func match(_ regex: String) -> [[String]] { + let nsString = self as NSString + return (try? NSRegularExpression(pattern: regex, options: []))?.matches(in: self, options: [], range: NSMakeRange(0, nsString.length)).map { match in + (0.. EKSource? { - let localSources = eventStore.sources.filter { $0.sourceType == .local } - - if (!localSources.isEmpty) { - return localSources.first - } - if let defaultSource = eventStore.defaultCalendarForNewEvents?.source { - return defaultSource - } - - let iCloudSources = eventStore.sources.filter { $0.sourceType == .calDAV && $0.sourceIdentifier == "iCloud" } + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case requestPermissionsMethod: + requestPermissions(result) + case hasPermissionsMethod: + hasPermissions(result) + case retrieveCalendarsMethod: + retrieveCalendars(result) + case retrieveEventsMethod: + retrieveEvents(call, result) + case createOrUpdateEventMethod: + createOrUpdateEvent(call, result) + case deleteEventMethod: + deleteEvent(call, result) + case deleteEventInstanceMethod: + deleteEvent(call, result) + case createCalendarMethod: + createCalendar(call, result) + case deleteCalendarMethod: + deleteCalendar(call, result) + default: + result(FlutterMethodNotImplemented) + } + } - if (!iCloudSources.isEmpty) { - return iCloudSources.first - } + private func hasPermissions(_ result: FlutterResult) { + let hasPermissions = hasEventPermissions() + result(hasPermissions) + } - return nil - } + private func getSource() -> EKSource? { + let localSources = eventStore.sources.filter { $0.sourceType == .local } - private func createCalendar(_ call: FlutterMethodCall, _ result: FlutterResult) { - let arguments = call.arguments as! Dictionary - let calendar = EKCalendar.init(for: EKEntityType.event, eventStore: eventStore) - 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 + if (!localSources.isEmpty) { + return localSources.first } - - guard let source = getSource() else { - result(FlutterError(code: self.genericError, message: "Local calendar was not found.", details: nil)) - return + + if let defaultSource = eventStore.defaultCalendarForNewEvents?.source { + return defaultSource } - calendar.source = source + let iCloudSources = eventStore.sources.filter { $0.sourceType == .calDAV && $0.sourceIdentifier == "iCloud" } - try eventStore.saveCalendar(calendar, commit: true) - result(calendar.calendarIdentifier) - } - catch { - eventStore.reset() - result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) + if (!iCloudSources.isEmpty) { + return iCloudSources.first + } + + return nil } - } - - private func retrieveCalendars(_ result: @escaping FlutterResult) { - checkPermissionsThenExecute(permissionsGrantedAction: { - let ekCalendars = self.eventStore.calendars(for: .event) - let defaultCalendar = self.eventStore.defaultCalendarForNewEvents - var calendars = [Calendar]() - for ekCalendar in ekCalendars { - let calendar = Calendar( - id: ekCalendar.calendarIdentifier, - name: ekCalendar.title, - isReadOnly: !ekCalendar.allowsContentModifications, - isDefault: defaultCalendar?.calendarIdentifier == ekCalendar.calendarIdentifier, - color: UIColor(cgColor: ekCalendar.cgColor).rgb()!, - accountName: ekCalendar.source.title, - 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: { + + private func createCalendar(_ call: FlutterMethodCall, _ result: FlutterResult) { 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 - } - + let calendar = EKCalendar.init(for: EKEntityType.event, eventStore: eventStore) do { - try self.eventStore.removeCalendar(ekCalendar!, commit: true) - result(true) - } catch { - self.eventStore.reset() + 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 + } + + calendar.source = source + + try eventStore.saveCalendar(calendar, commit: true) + result(calendar.calendarIdentifier) + } + catch { + eventStore.reset() result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) } - }, result: result) - } - - private func getAccountType(_ sourceType: EKSourceType) -> String { - switch (sourceType) { - case .local: - return "Local"; - case .exchange: - return "Exchange"; - case .calDAV: - return "CalDAV"; - case .mobileMe: - return "MobileMe"; - case .subscribed: - return "Subscribed"; - case .birthdays: - return "Birthdays"; - default: - return "Unknown"; } - } - - private func retrieveEvents(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - checkPermissionsThenExecute(permissionsGrantedAction: { - let arguments = call.arguments as! Dictionary - let calendarId = arguments[calendarIdArgument] as! String - let startDateMillisecondsSinceEpoch = arguments[startDateArgument] as? NSNumber - let endDateDateMillisecondsSinceEpoch = arguments[endDateArgument] as? NSNumber - let eventIdArgs = arguments[eventIdsArgument] as? [String] - var events = [Event]() - let specifiedStartEndDates = startDateMillisecondsSinceEpoch != nil && endDateDateMillisecondsSinceEpoch != nil - if specifiedStartEndDates { - let startDate = Date (timeIntervalSince1970: startDateMillisecondsSinceEpoch!.doubleValue / 1000.0) - let endDate = Date (timeIntervalSince1970: endDateDateMillisecondsSinceEpoch!.doubleValue / 1000.0) + + private func retrieveCalendars(_ result: @escaping FlutterResult) { + checkPermissionsThenExecute(permissionsGrantedAction: { + let ekCalendars = self.eventStore.calendars(for: .event) + let defaultCalendar = self.eventStore.defaultCalendarForNewEvents + var calendars = [DeviceCalendar]() + for ekCalendar in ekCalendars { + let calendar = DeviceCalendar( + id: ekCalendar.calendarIdentifier, + name: ekCalendar.title, + isReadOnly: !ekCalendar.allowsContentModifications, + isDefault: defaultCalendar?.calendarIdentifier == ekCalendar.calendarIdentifier, + color: UIColor(cgColor: ekCalendar.cgColor).rgb()!, + accountName: ekCalendar.source.title, + 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 { - let predicate = self.eventStore.predicateForEvents( - withStart: startDate, - end: endDate, - calendars: [ekCalendar!]) - let ekEvents = self.eventStore.events(matching: predicate) - for ekEvent in ekEvents { - let event = createEventFromEkEvent(calendarId: calendarId, ekEvent: ekEvent) - events.append(event) - } + 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) + } catch { + self.eventStore.reset() + result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) + } + }, result: result) + } + + private func getAccountType(_ sourceType: EKSourceType) -> String { + switch (sourceType) { + case .local: + return "Local"; + case .exchange: + return "Exchange"; + case .calDAV: + return "CalDAV"; + case .mobileMe: + return "MobileMe"; + case .subscribed: + return "Subscribed"; + case .birthdays: + return "Birthdays"; + default: + return "Unknown"; } - - 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 + } + + private func retrieveEvents(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + checkPermissionsThenExecute(permissionsGrantedAction: { + let arguments = call.arguments as! Dictionary + let calendarId = arguments[calendarIdArgument] as! String + let startDateMillisecondsSinceEpoch = arguments[startDateArgument] as? NSNumber + let endDateDateMillisecondsSinceEpoch = arguments[endDateArgument] as? NSNumber + let eventIdArgs = arguments[eventIdsArgument] as? [String] + var events = [Event]() + let specifiedStartEndDates = startDateMillisecondsSinceEpoch != nil && endDateDateMillisecondsSinceEpoch != nil + if specifiedStartEndDates { + let startDate = Date (timeIntervalSince1970: startDateMillisecondsSinceEpoch!.doubleValue / 1000.0) + 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) + for ekEvent in ekEvents { + let event = createEventFromEkEvent(calendarId: calendarId, ekEvent: ekEvent) + events.append(event) + } + } + } + + 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 +371,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,33 +405,33 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele return event } - - private func convertEkParticipantToAttendee(ekParticipant: EKParticipant?) -> Attendee? { - if ekParticipant == nil || ekParticipant?.emailAddress == nil { - return nil + + private func convertEkParticipantToAttendee(ekParticipant: EKParticipant?) -> Attendee? { + if ekParticipant == nil || ekParticipant?.emailAddress == nil { + return nil + } + + let attendee = Attendee( + name: ekParticipant!.name, + emailAddress: ekParticipant!.emailAddress!, + role: ekParticipant!.participantRole.rawValue, + attendanceStatus: ekParticipant!.participantStatus.rawValue, + isCurrentUser: ekParticipant!.isCurrentUser + ) + + return attendee } - - let attendee = Attendee( - name: ekParticipant!.name, - emailAddress: ekParticipant!.emailAddress!, - role: ekParticipant!.participantRole.rawValue, - attendanceStatus: ekParticipant!.participantStatus.rawValue, - isCurrentUser: ekParticipant!.isCurrentUser - ) - - return attendee - } - + private func convertEkEventAvailability(ekEventAvailability: EKEventAvailability?) -> Availability? { switch ekEventAvailability { case .busy: - return Availability.BUSY + return Availability.BUSY case .free: return Availability.FREE - case .tentative: - return Availability.TENTATIVE - case .unavailable: - return Availability.UNAVAILABLE + case .tentative: + return Availability.TENTATIVE + case .unavailable: + return Availability.UNAVAILABLE default: return nil } @@ -441,167 +456,217 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele var recurrenceRule: RecurrenceRule? if ekEvent.hasRecurrenceRules { let ekRecurrenceRule = ekEvent.recurrenceRules![0] - var frequency: Int + var frequency: String switch ekRecurrenceRule.frequency { case EKRecurrenceFrequency.daily: - frequency = 0 + frequency = "DAILY" case EKRecurrenceFrequency.weekly: - frequency = 1 + frequency = "WEEKLY" case EKRecurrenceFrequency.monthly: - frequency = 2 + frequency = "MONTHLY" case EKRecurrenceFrequency.yearly: - frequency = 3 + frequency = "YEARLY" default: - frequency = 0 + frequency = "DAILY" } - - var totalOccurrences: Int? - var endDate: Int64? + + var count: Int? + var endDate: String? 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 - } - } + count = ekRecurrenceRule.recurrenceEnd?.occurrenceCount + } + + let endDateRaw = ekRecurrenceRule.recurrenceEnd?.endDate + if(endDateRaw != nil) { + endDate = formateDateTime(dateTime: endDateRaw!) } - - // 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 - var monthOfYear = ekRecurrenceRule.monthsOfTheYear?.first?.intValue - 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)) - } - + + let byWeekDays = ekRecurrenceRule.daysOfTheWeek + let byMonthDays = ekRecurrenceRule.daysOfTheMonth + let byYearDays = ekRecurrenceRule.daysOfTheYear + let byWeeks = ekRecurrenceRule.weeksOfTheYear + let byMonths = ekRecurrenceRule.monthsOfTheYear + let bySetPositions = ekRecurrenceRule.setPositions + recurrenceRule = RecurrenceRule( - recurrenceFrequency: frequency, - totalOccurrences: totalOccurrences, + freq: frequency, + count: count, interval: ekRecurrenceRule.interval, - endDate: endDate, - daysOfWeek: daysOfWeek, - dayOfMonth: dayOfMonth, - monthOfYear: monthOfYear, - weekOfMonth: weekOfMonth) + until: endDate, + byday: byWeekDays?.map {weekDayToString($0)}, + bymonthday: byMonthDays?.map {Int(truncating: $0)}, + byyearday: byYearDays?.map {Int(truncating: $0)}, + byweekno: byWeeks?.map {Int(truncating: $0)}, + bymonth: byMonths?.map {Int(truncating: $0)}, + bysetpos: bySetPositions?.map {Int(truncating: $0)}, + sourceRruleString: rruleStringFromEKRRule(ekRecurrenceRule) + ) } - + //print("RECURRENCERULE_RESULT: \(recurrenceRule as AnyObject)") return recurrenceRule } - + + private func weekDayToString(_ entry : EKRecurrenceDayOfWeek) -> String { + let weekNumber = entry.weekNumber + let day = dayValueToString(entry.dayOfTheWeek.rawValue) + if (weekNumber == 0) { + return "\(day)" + } else { + return "\(weekNumber)\(day)" + } + } + + private func dayValueToString(_ day: Int) -> String { + switch day { + case 1: return "SU" + case 2: return "MO" + case 3: return "TU" + case 4: return "WE" + case 5: return "TH" + case 6: return "FR" + case 7: return "SA" + default: return "SU" + } + } + + private func formateDateTime(dateTime: Date) -> String { + var calendar = Calendar.current + calendar.timeZone = TimeZone.current + + func twoDigits(_ n: Int) -> String { + if (n < 10) {return "0\(n)"} else {return "\(n)"} + } + + func fourDigits(_ n: Int) -> String { + let absolute = abs(n) + let sign = n < 0 ? "-" : "" + if (absolute >= 1000) {return "\(n)"} + if (absolute >= 100) {return "\(sign)0\(absolute)"} + if (absolute >= 10) {return "\(sign)00\(absolute)"} + return "\(sign)000\(absolute)" + } + + let year = calendar.component(.year, from: dateTime) + let month = calendar.component(.month, from: dateTime) + let day = calendar.component(.day, from: dateTime) + let hour = calendar.component(.hour, from: dateTime) + let minutes = calendar.component(.minute, from: dateTime) + let seconds = calendar.component(.second, from: dateTime) + + assert(year >= 0 && year <= 9999) + + let yearString = fourDigits(year) + let monthString = twoDigits(month) + let dayString = twoDigits(day) + let hourString = twoDigits(hour) + let minuteString = twoDigits(minutes) + let secondString = twoDigits(seconds) + let utcSuffix = calendar.timeZone == TimeZone(identifier: "UTC") ? "Z" : "" + return "\(yearString)-\(monthString)-\(dayString)T\(hourString):\(minuteString):\(secondString)\(utcSuffix)" + + } + private func createEKRecurrenceRules(_ arguments: [String : AnyObject]) -> [EKRecurrenceRule]?{ let recurrenceRuleArguments = arguments[recurrenceRuleArgument] as? Dictionary + + //print("ARGUMENTS: \(recurrenceRuleArguments as AnyObject)") + if recurrenceRuleArguments == nil { return nil } - - let recurrenceFrequencyIndex = recurrenceRuleArguments![recurrenceFrequencyArgument] as? NSInteger - let totalOccurrences = recurrenceRuleArguments![totalOccurrencesArgument] as? NSInteger + + let recurrenceFrequency = recurrenceRuleArguments![recurrenceFrequencyArgument] as? String + let totalOccurrences = recurrenceRuleArguments![countArgument] as? NSInteger let interval = recurrenceRuleArguments![intervalArgument] as? NSInteger var recurrenceInterval = 1 - let endDate = recurrenceRuleArguments![endDateArgument] as? NSNumber - let namedFrequency = validFrequencyTypes[recurrenceFrequencyIndex!] - - var recurrenceEnd:EKRecurrenceEnd? + var endDate = recurrenceRuleArguments![untilArgument] as? String + var namedFrequency: EKRecurrenceFrequency + switch recurrenceFrequency { + case "YEARLY": + namedFrequency = EKRecurrenceFrequency.yearly + case "MONTHLY": + namedFrequency = EKRecurrenceFrequency.monthly + case "WEEKLY": + namedFrequency = EKRecurrenceFrequency.weekly + case "DAILY": + namedFrequency = EKRecurrenceFrequency.daily + default: + namedFrequency = EKRecurrenceFrequency.daily + } + + var recurrenceEnd: EKRecurrenceEnd? if endDate != nil { - recurrenceEnd = EKRecurrenceEnd(end: Date.init(timeIntervalSince1970: endDate!.doubleValue / 1000)) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + + if (!endDate!.hasSuffix("Z")){ + endDate!.append("Z") + } + + let dateTime = dateFormatter.date(from: endDate!) + if dateTime != nil { + recurrenceEnd = EKRecurrenceEnd(end: dateTime!) + } } 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! { - // Append week number to BYDAY for yearly or monthly with 'last' week number - if let weekOfMonth = recurrenceRuleArguments![weekOfMonthArgument] as? Int { - if namedFrequency == EKRecurrenceFrequency.yearly || weekOfMonth == -1 { - daysOfWeek!.append(EKRecurrenceDayOfWeek.init( - dayOfTheWeek: EKWeekday.init(rawValue: dayOfWeekIndex + 1)!, - weekNumber: weekOfMonth - )) - } - } else { - daysOfWeek!.append(EKRecurrenceDayOfWeek.init(EKWeekday.init(rawValue: dayOfWeekIndex + 1)!)) - } - } - } - - 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 { - if let weekOfMonth = recurrenceRuleArguments![weekOfMonthArgument] as? Int { - if weekOfMonth != -1 { - weekOfMonthArray = [] - weekOfMonthArray!.append(NSNumber(value: weekOfMonth)) - } + + let byWeekDaysStrings = recurrenceRuleArguments![byWeekDaysArgument] as? [String] + var byWeekDays = [EKRecurrenceDayOfWeek]() + + if (byWeekDaysStrings != nil) { + byWeekDaysStrings?.forEach { string in + let entry = recurrenceDayOfWeekFromString(recDay: string) + if entry != nil {byWeekDays.append(entry!)} } } - - return [EKRecurrenceRule( + + let byMonthDays = recurrenceRuleArguments![byMonthDaysArgument] as? [Int] + let byYearDays = recurrenceRuleArguments![byYearDaysArgument] as? [Int] + let byWeeks = recurrenceRuleArguments![byWeeksArgument] as? [Int] + let byMonths = recurrenceRuleArguments![byMonthsArgument] as? [Int] + let bySetPositions = recurrenceRuleArguments![bySetPositionsArgument] as? [Int] + + let ekrecurrenceRule = EKRecurrenceRule( recurrenceWith: namedFrequency, interval: recurrenceInterval, - daysOfTheWeek: daysOfWeek, - daysOfTheMonth: dayOfMonthArray, - monthsOfTheYear: monthOfYearArray, - weeksOfTheYear: nil, - daysOfTheYear: nil, - setPositions: weekOfMonthArray, - end: recurrenceEnd)] + daysOfTheWeek: byWeekDays.isEmpty ? nil : byWeekDays, + daysOfTheMonth: byMonthDays?.map {NSNumber(value: $0)}, + monthsOfTheYear: byMonths?.map {NSNumber(value: $0)}, + weeksOfTheYear: byWeeks?.map {NSNumber(value: $0)}, + daysOfTheYear: byYearDays?.map {NSNumber(value: $0)}, + setPositions: bySetPositions?.map {NSNumber(value: $0)}, + end: recurrenceEnd) + //print("ekrecurrenceRule: \(String(describing: ekrecurrenceRule))") + return [ekrecurrenceRule] } - + + private func rruleStringFromEKRRule(_ ekRrule: EKRecurrenceRule) -> String { + let ekRRuleAnyObject = ekRrule as AnyObject + var ekRRuleString = "\(ekRRuleAnyObject)" + if let range = ekRRuleString.range(of: "RRULE ") { + ekRRuleString = String(ekRRuleString[range.upperBound...]) + //print("EKRULE_RESULT_STRING: \(ekRRuleString)") + } + return ekRRuleString + } + 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 +676,88 @@ 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 recurrenceDayOfWeekFromString(recDay: String) -> EKRecurrenceDayOfWeek? { + let results = recDay.match("(?:(\\+|-)?([0-9]{1,2}))?([A-Za-z]{2})").first + var recurrenceDayOfWeek : EKRecurrenceDayOfWeek? + if (results != nil) { + var occurrence : Int? + let numberMatch = results![2] + if (!numberMatch.isEmpty) { + occurrence = Int(numberMatch) + if (1 > occurrence! || occurrence! > 53) { + print("OCCURRENCE_ERROR: OUT OF RANGE -> \(String(describing: occurrence))") + } + if (results![1] == "-") { + occurrence = -occurrence! + } + } + let dayMatch = results![3] + + var weekday = EKWeekday.monday + + switch dayMatch { + case "MO": + weekday = EKWeekday.monday + case "TU": + weekday = EKWeekday.tuesday + case "WE": + weekday = EKWeekday.wednesday + case "TH": + weekday = EKWeekday.thursday + case "FR": + weekday = EKWeekday.friday + case "SA": + weekday = EKWeekday.saturday + case "SU": + weekday = EKWeekday.sunday + default: + weekday = EKWeekday.sunday + } + + if occurrence != nil { + recurrenceDayOfWeek = EKRecurrenceDayOfWeek(dayOfTheWeek: weekday, weekNumber: occurrence!) + } else { + recurrenceDayOfWeek = EKRecurrenceDayOfWeek(weekday) + } + } + return recurrenceDayOfWeek + } + + 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() { @@ -652,15 +765,15 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele return .busy case Availability.FREE.rawValue: return .free - case Availability.TENTATIVE.rawValue: - return .tentative + case Availability.TENTATIVE.rawValue: + return .tentative case Availability.UNAVAILABLE.rawValue: return .unavailable default: return nil } } - + private func createOrUpdateEvent(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { checkPermissionsThenExecute(permissionsGrantedAction: { let arguments = call.arguments as! Dictionary @@ -681,12 +794,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,7 +810,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele return } } - + ekEvent!.title = title ekEvent!.notes = description ekEvent!.isAllDay = isAllDay @@ -705,7 +818,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele if (isAllDay) { ekEvent!.endDate = startDate } else { ekEvent!.endDate = endDate - + let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current ekEvent!.timeZone = timeZone } @@ -720,15 +833,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 +851,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 +863,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 +872,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 +902,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 +920,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele else { try self.eventStore.remove(ekEvent!, span: .futureEvents, commit: true) } - + result(true) } catch { self.eventStore.reset() @@ -817,80 +930,80 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele }, result: result) } - private func showEventModal(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - checkPermissionsThenExecute(permissionsGrantedAction: { - 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: - flutterResult!(nil) - case .responded: - flutterResult!(nil) - case .deleted: - flutterResult!(nil) - @unknown default: - flutterResult!(nil) + private func showEventModal(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + checkPermissionsThenExecute(permissionsGrantedAction: { + 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: + flutterResult!(nil) + case .responded: + flutterResult!(nil) + case .deleted: + flutterResult!(nil) + @unknown default: + flutterResult!(nil) + } } } - } - - private func getTopMostViewController() -> UIViewController { - var topController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController - while ((topController?.presentedViewController) != nil) { - topController = topController?.presentedViewController - } - - return topController! - } - + + 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 +1014,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 +1022,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } self.finishWithUnauthorizedError(result: result) } - + private func requestPermissions(completion: @escaping (Bool) -> Void) { if hasEventPermissions() { completion(true) @@ -920,12 +1033,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 +1077,7 @@ extension UIColor { return nil } } - + public convenience init?(hex: String) { let r, g, b, a: CGFloat @@ -981,7 +1094,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 } @@ -990,5 +1103,5 @@ extension UIColor { return nil } -} +} diff --git a/lib/device_calendar.dart b/lib/device_calendar.dart index 15d51182..b25b1f29 100644 --- a/lib/device_calendar.dart +++ b/lib/device_calendar.dart @@ -1,14 +1,14 @@ library device_calendar; export 'src/common/calendar_enums.dart'; -export 'src/common/recurrence_frequency.dart'; export 'src/models/attendee.dart'; export 'src/models/calendar.dart'; export 'src/models/result.dart'; export 'src/models/reminder.dart'; export 'src/models/event.dart'; export 'src/models/retrieve_events_params.dart'; -export 'src/models/recurrence_rule.dart'; +export 'package:rrule/rrule.dart'; +export 'package:rrule/src/frequency.dart'; export 'src/models/platform_specifics/ios/attendee_details.dart'; export 'src/models/platform_specifics/ios/attendance_status.dart'; export 'src/models/platform_specifics/android/attendee_details.dart'; diff --git a/lib/src/common/calendar_enums.dart b/lib/src/common/calendar_enums.dart index 2e7cd246..aa77ec02 100644 --- a/lib/src/common/calendar_enums.dart +++ b/lib/src/common/calendar_enums.dart @@ -86,6 +86,7 @@ extension DayOfWeekExtension on DayOfWeek { } int get value => _value(this); + String get enumToString => _enumToString(this); } @@ -122,6 +123,7 @@ extension DaysOfWeekGroupExtension on DayOfWeekGroup { } List get getDays => _getDays(this); + String get enumToString => _enumToString(this); } @@ -162,6 +164,7 @@ extension MonthOfYearExtension on MonthOfYear { } int get value => _value(this); + String get enumToString => _enumToString(this); } @@ -188,6 +191,7 @@ extension WeekNumberExtension on WeekNumber { } int get value => _value(this); + String get enumToString => _enumToString(this); } @@ -262,7 +266,9 @@ extension IntExtensions on int { } DayOfWeek get getDayOfWeekEnumValue => _getDayOfWeekEnumValue(this); + MonthOfYear get getMonthOfYearEnumValue => _getMonthOfYearEnumValue(this); + WeekNumber get getWeekNumberEnumValue => _getWeekNumberEnumValue(this); } @@ -306,4 +312,4 @@ extension EventStatusExtensions on EventStatus { } String get enumToString => _enumToString(this); -} \ No newline at end of file +} diff --git a/lib/src/common/recurrence_frequency.dart b/lib/src/common/recurrence_frequency.dart deleted file mode 100644 index 176cf3de..00000000 --- a/lib/src/common/recurrence_frequency.dart +++ /dev/null @@ -1,6 +0,0 @@ -enum RecurrenceFrequency { - Daily, - Weekly, - Monthly, - Yearly, -} diff --git a/lib/src/device_calendar.dart b/lib/src/device_calendar.dart index eb1d9db1..709aec2a 100644 --- a/lib/src/device_calendar.dart +++ b/lib/src/device_calendar.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:sprintf/sprintf.dart'; import 'package:timezone/data/latest.dart' as tz; import 'package:timezone/timezone.dart'; @@ -80,42 +79,48 @@ class DeviceCalendarPlugin { String? calendarId, RetrieveEventsParams? retrieveEventsParams, ) async { - return _invokeChannelMethod( - ChannelConstants.methodNameRetrieveEvents, - assertParameters: (result) { - _validateCalendarIdParameter( - result, - calendarId, - ); - - _assertParameter( - result, - !((retrieveEventsParams?.eventIds?.isEmpty ?? true) && - ((retrieveEventsParams?.startDate == null || - retrieveEventsParams?.endDate == null) || - (retrieveEventsParams?.startDate != null && - retrieveEventsParams?.endDate != null && - (retrieveEventsParams != null && - retrieveEventsParams.startDate! - .isAfter(retrieveEventsParams.endDate!))))), - ErrorCodes.invalidArguments, - ErrorMessages.invalidRetrieveEventsParams, - ); - }, - arguments: () => { - ChannelConstants.parameterNameCalendarId: calendarId, - ChannelConstants.parameterNameStartDate: - retrieveEventsParams?.startDate?.millisecondsSinceEpoch, - ChannelConstants.parameterNameEndDate: - retrieveEventsParams?.endDate?.millisecondsSinceEpoch, - ChannelConstants.parameterNameEventIds: retrieveEventsParams?.eventIds, - }, - evaluateResponse: (rawData) => UnmodifiableListView( + return _invokeChannelMethod(ChannelConstants.methodNameRetrieveEvents, + assertParameters: (result) { + _validateCalendarIdParameter( + result, + calendarId, + ); + + _assertParameter( + result, + !((retrieveEventsParams?.eventIds?.isEmpty ?? true) && + ((retrieveEventsParams?.startDate == null || + retrieveEventsParams?.endDate == null) || + (retrieveEventsParams?.startDate != null && + retrieveEventsParams?.endDate != null && + (retrieveEventsParams != null && + retrieveEventsParams.startDate! + .isAfter(retrieveEventsParams.endDate!))))), + ErrorCodes.invalidArguments, + ErrorMessages.invalidRetrieveEventsParams, + ); + }, + arguments: () => { + ChannelConstants.parameterNameCalendarId: calendarId, + ChannelConstants.parameterNameStartDate: + retrieveEventsParams?.startDate?.millisecondsSinceEpoch, + ChannelConstants.parameterNameEndDate: + retrieveEventsParams?.endDate?.millisecondsSinceEpoch, + ChannelConstants.parameterNameEventIds: + retrieveEventsParams?.eventIds, + }, + /*evaluateResponse: (rawData) => UnmodifiableListView( json .decode(rawData) .map((decodedEvent) => Event.fromJson(decodedEvent)), - ), - ); + ),*/ + evaluateResponse: (rawData) => UnmodifiableListView( + json.decode(rawData).map((decodedEvent) { + // debugPrint( + // "JSON_RRULE: ${decodedEvent['recurrenceRule']}, ${(decodedEvent['recurrenceRule']['byday'])}"); + return Event.fromJson(decodedEvent); + }), + )); } /// Deletes an event from a calendar. For a recurring event, this will delete all instances of it.\ @@ -214,9 +219,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 +231,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(Duration(days: 1)) : TZDateTime.from(dateEnd, - timeZoneDatabase.locations[event.end!.location.name]!); + timeZoneDatabase.locations[event.end!.location.name]!); } } @@ -322,7 +327,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 +342,6 @@ class DeviceCalendarPlugin { ); } - Future> _invokeChannelMethod( String channelMethodName, { Function(Result)? assertParameters, @@ -364,8 +368,15 @@ class DeviceCalendarPlugin { } else { result.data = rawData; } - } catch (e) { - _parsePlatformExceptionAndUpdateResult(e as Exception?, result); + } catch (e, s) { + if (e is ArgumentError) { + debugPrint( + "INVOKE_CHANNEL_METHOD_ERROR! Name: ${e.name}, InvalidValue: ${e.invalidValue}, Message: ${e.message}, ${e.toString()}"); + } else if (e is PlatformException) { + debugPrint('INVOKE_CHANNEL_METHOD_ERROR: $e\n$s'); + } else { + _parsePlatformExceptionAndUpdateResult(e as Exception?, result); + } } return result; @@ -375,7 +386,7 @@ class DeviceCalendarPlugin { Exception? exception, Result result) { if (exception == null) { result.errors.add( - ResultError( + const ResultError( ErrorCodes.unknown, ErrorMessages.unknownDeviceIssue, ), @@ -383,22 +394,20 @@ class DeviceCalendarPlugin { return; } - print(exception); + debugPrint('$exception'); if (exception is PlatformException) { result.errors.add( ResultError( ErrorCodes.platformSpecific, - sprintf(ErrorMessages.unknownDeviceExceptionTemplate, - [exception.code, exception.message]), + '${ErrorMessages.unknownDeviceExceptionTemplate}, Code: ${exception.code}, Exception: ${exception.message}', ), ); } else { result.errors.add( ResultError( ErrorCodes.generic, - sprintf(ErrorMessages.unknownDeviceGenericExceptionTemplate, - [exception.toString()]), + '${ErrorMessages.unknownDeviceGenericExceptionTemplate} ${exception.toString}', ), ); } @@ -410,6 +419,9 @@ class DeviceCalendarPlugin { int errorCode, String errorMessage, ) { + if (result.data != null) { + debugPrint("RESULT of _assertParameter: ${result.data}"); + } if (!predicate) { result.errors.add( ResultError(errorCode, errorMessage), diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index bc3e2f2d..93529c38 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -1,9 +1,10 @@ import 'dart:io'; +import 'package:collection/collection.dart'; +import 'package:timezone/timezone.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 +49,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. @@ -82,6 +83,7 @@ class Event { if (json == null) { throw ArgumentError(ErrorMessages.fromJsonMapIsNull); } + String? foundUrl; String? startLocationName; String? endLocationName; @@ -170,7 +172,42 @@ class Event { } if (json['recurrenceRule'] != null) { + // debugPrint( + // "EVENT_MODEL: $title; START: $start, END: $end RRULE = ${json['recurrenceRule']}"); + + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'byday'') + if (json['recurrenceRule']['byday'] != null) { + json['recurrenceRule']['byday'] = + json['recurrenceRule']['byday'].cast(); + } + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'bymonthday'') + if (json['recurrenceRule']['bymonthday'] != null) { + json['recurrenceRule']['bymonthday'] = + json['recurrenceRule']['bymonthday'].cast(); + } + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'byyearday'') + if (json['recurrenceRule']['byyearday'] != null) { + json['recurrenceRule']['byyearday'] = + json['recurrenceRule']['byyearday'].cast(); + } + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'byweekno'') + if (json['recurrenceRule']['byweekno'] != null) { + json['recurrenceRule']['byweekno'] = + json['recurrenceRule']['byweekno'].cast(); + } + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'bymonth'') + if (json['recurrenceRule']['bymonth'] != null) { + json['recurrenceRule']['bymonth'] = + json['recurrenceRule']['bymonth'].cast(); + } + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'bysetpos'') + if (json['recurrenceRule']['bysetpos'] != null) { + json['recurrenceRule']['bysetpos'] = + json['recurrenceRule']['bysetpos'].cast(); + } + // debugPrint("EVENT_MODEL: $title; RRULE = ${json['recurrenceRule']}"); recurrenceRule = RecurrenceRule.fromJson(json['recurrenceRule']); + // debugPrint("EVENT_MODEL_recurrenceRule: ${recurrenceRule.toString()}"); } if (json['reminders'] != null) { @@ -214,12 +251,13 @@ class Event { if (recurrenceRule != null) { data['recurrenceRule'] = recurrenceRule?.toJson(); + // print("EVENT_TO_JSON_RRULE: ${recurrenceRule?.toJson()}"); } if (reminders != null) { data['reminders'] = reminders?.map((r) => r.toJson()).toList(); } - + // debugPrint("EVENT_TO_JSON: $data"); return data; } @@ -250,6 +288,7 @@ class Event { case 'NONE': return EventStatus.None; } + return null; } bool updateStartLocation(String? newStartLocation) { diff --git a/lib/src/models/recurrence_rule.dart b/lib/src/models/recurrence_rule.dart deleted file mode 100644 index 02825262..00000000 --- a/lib/src/models/recurrence_rule.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'package:device_calendar/src/common/calendar_enums.dart'; - -import '../common/error_messages.dart'; -import '../common/recurrence_frequency.dart'; - -class RecurrenceRule { - int? totalOccurrences; - - /// The interval between instances of a recurring event - int? interval; - - /// The date a series of recurring events should end - DateTime? endDate; - - /// The frequency of recurring events - RecurrenceFrequency? recurrenceFrequency; - - /// The days of the week that this event occurs on. Only applicable to recurrence rules with a weekly, monthly or yearly frequency - List? daysOfWeek = []; - - /// A day of the month that this event occurs on. Only applicable to recurrence rules with a monthly or yearly frequency - int? dayOfMonth; - - /// A month of the year that the event occurs on. Only applicable to recurrence rules with a yearly frequency - MonthOfYear? monthOfYear; - - /// Filters which recurrences to include in the recurrence rule’s frequency. Only applicable when _isByDayOfMonth is false - WeekNumber? weekOfMonth; - - final String _totalOccurrencesKey = 'totalOccurrences'; - final String _recurrenceFrequencyKey = 'recurrenceFrequency'; - final String _intervalKey = 'interval'; - final String _endDateKey = 'endDate'; - final String _daysOfWeekKey = 'daysOfWeek'; - final String _dayOfMonthKey = 'dayOfMonth'; - final String _monthOfYearKey = 'monthOfYear'; - final String _weekOfMonthKey = 'weekOfMonth'; - - RecurrenceRule(this.recurrenceFrequency, - {this.totalOccurrences, - this.interval, - this.endDate, - this.daysOfWeek, - this.dayOfMonth, - this.monthOfYear, - this.weekOfMonth}) - : assert(!(endDate != null && totalOccurrences != null), - 'Cannot specify both an end date and total occurrences for a recurring event'); - - RecurrenceRule.fromJson(Map? json) { - if (json == null) { - throw ArgumentError(ErrorMessages.fromJsonMapIsNull); - } - - int? recurrenceFrequencyIndex = json[_recurrenceFrequencyKey]; - if (recurrenceFrequencyIndex == null || - recurrenceFrequencyIndex >= RecurrenceFrequency.values.length) { - throw ArgumentError(ErrorMessages.invalidRecurrencyFrequency); - } - recurrenceFrequency = RecurrenceFrequency.values[recurrenceFrequencyIndex]; - - totalOccurrences = json[_totalOccurrencesKey]; - - interval = json[_intervalKey]; - - int? endDateMillisecondsSinceEpoch = json[_endDateKey]; - if (endDateMillisecondsSinceEpoch != null) { - endDate = - DateTime.fromMillisecondsSinceEpoch(endDateMillisecondsSinceEpoch); - } - - List? daysOfWeekValues = json[_daysOfWeekKey]; - if (daysOfWeekValues != null && daysOfWeekValues is! List) { - daysOfWeek = daysOfWeekValues - .cast() - .map((value) => value.getDayOfWeekEnumValue) - .toList(); - } - - dayOfMonth = json[_dayOfMonthKey]; - monthOfYear = - convertDynamicToInt(json[_monthOfYearKey])?.getMonthOfYearEnumValue; - weekOfMonth = - convertDynamicToInt(json[_weekOfMonthKey])?.getWeekNumberEnumValue; - } - - int? convertDynamicToInt(dynamic value) { - value = value?.toString(); - return value != null ? int.tryParse(value) : null; - } - - Map toJson() { - final data = {}; - - if (totalOccurrences != null) { - data[_totalOccurrencesKey] = totalOccurrences; - } - - if (interval != null) { - data[_intervalKey] = interval; - } - - if (endDate != null) { - data[_endDateKey] = endDate!.millisecondsSinceEpoch; - } - - data[_recurrenceFrequencyKey] = recurrenceFrequency?.index; - - if (daysOfWeek?.isNotEmpty == true) { - data[_daysOfWeekKey] = daysOfWeek!.map((d) => d.value).toList(); - } - - if (monthOfYear != null && - recurrenceFrequency == RecurrenceFrequency.Yearly) { - data[_monthOfYearKey] = monthOfYear!.value; - } - - if (recurrenceFrequency == RecurrenceFrequency.Monthly || - recurrenceFrequency == RecurrenceFrequency.Yearly) { - if (weekOfMonth != null) { - data[_weekOfMonthKey] = weekOfMonth!.value; - } else { - // Days of the month should not be added to the recurrence parameter when WeekOfMonth is used - if (dayOfMonth != null) { - data[_dayOfMonthKey] = dayOfMonth; - } - } - } - - return data; - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 19b57aeb..42aae1ff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,13 +10,14 @@ dependencies: sprintf: ^6.0.2 timezone: ^0.9.0 flutter_native_timezone: ^2.0.0 + intl: ^0.17.0 + rrule: ^0.2.7 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.1 -# The following section is specific to Flutter. flutter: plugin: platforms: @@ -28,4 +29,4 @@ flutter: environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13" + flutter: ">=1.20.0" diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart index 6c105c0a..acc26441 100644 --- a/test/device_calendar_test.dart +++ b/test/device_calendar_test.dart @@ -198,7 +198,7 @@ void main() { emailAddress: 'test@t.com', role: AttendeeRole.Required, isOrganiser: true); - final recurrence = RecurrenceRule(RecurrenceFrequency.Daily); + final recurrence = RecurrenceRule(frequency: Frequency.daily); final reminder = Reminder(minutes: 10); var event = Event('calendarId', eventId: 'eventId', @@ -231,8 +231,8 @@ void main() { expect(newEvent.attendees, isNotNull); expect(newEvent.attendees?.length, equals(1)); expect(newEvent.recurrenceRule, isNotNull); - expect(newEvent.recurrenceRule?.recurrenceFrequency, - equals(event.recurrenceRule?.recurrenceFrequency)); + expect(newEvent.recurrenceRule?.frequency, + equals(event.recurrenceRule?.frequency)); expect(newEvent.reminders, isNotNull); expect(newEvent.reminders?.length, equals(1)); expect(newEvent.availability, equals(event.availability)); From f7e30e366be651556af67d928edcc9ee9072da0b Mon Sep 17 00:00:00 2001 From: thomassth Date: Fri, 30 Sep 2022 16:38:21 -0400 Subject: [PATCH 02/45] fix for ownerAccount null --- .../com/builttoroam/devicecalendar/models/Calendar.kt | 9 +-------- example/android/app/build.gradle | 2 +- example/android/build.gradle | 2 +- 3 files changed, 3 insertions(+), 10 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 09380c22..c7e3f6bf 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt @@ -1,13 +1,6 @@ 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 0b9bc9803670371bb6ac557b633b31d0d816d597 Mon Sep 17 00:00:00 2001 From: thomassth Date: Fri, 30 Sep 2022 16:55:45 -0400 Subject: [PATCH 03/45] 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 5c21d1d4..1113480d 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 10c8d3a8a2b771fa5c0805242f532ea3f3faa5c7 Mon Sep 17 00:00:00 2001 From: thomassth Date: Sat, 1 Oct 2022 01:19:38 -0400 Subject: [PATCH 04/45] 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 ce09d123d296436b8390bd77e25f3082682387a3 Mon Sep 17 00:00:00 2001 From: thomassth Date: Thu, 6 Oct 2022 01:54:24 -0400 Subject: [PATCH 05/45] activate flutter lint --- analysis_options.yaml | 30 ++++++++ example/analysis_options.yaml | 33 ++++++++- 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 | 26 +++---- .../presentation/pages/calendar_events.dart | 24 +++---- 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 | 2 +- lib/src/models/event.dart | 4 +- test/device_calendar_test.dart | 37 +++++----- 20 files changed, 180 insertions(+), 123 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 88d793de..68a79339 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1,3 +1,30 @@ -include: package:pedantic/analysis_options.yaml -#include: package:flutter_lints/flutter.yaml -# TODO: change to flutter lints (https://pub.dev/packages/flutter_lints) \ 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 \ No newline at end of file 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 bc002fd9..a18613a6 100644 --- a/example/lib/presentation/event_item.dart +++ b/example/lib/presentation/event_item.dart @@ -17,7 +17,7 @@ class EventItem extends StatefulWidget { final VoidCallback _onLoadingStarted; final Function(bool) _onDeleteFinished; - EventItem( + const EventItem( this._calendarEvent, this._deviceCalendarPlugin, this._onLoadingStarted, @@ -55,15 +55,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) @@ -71,9 +71,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 @@ -85,7 +85,7 @@ class _EventItemState extends State { ], ), ), - Padding( + const Padding( padding: EdgeInsets.symmetric(vertical: 5.0), ), if (_currentLocation != null) @@ -93,9 +93,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 @@ -107,16 +107,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 @@ -125,16 +125,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( @@ -145,16 +145,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( @@ -165,16 +165,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( @@ -189,16 +189,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( @@ -210,16 +210,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 b54dacfb..7d6d8820 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 116cbec0..1faa8dbb 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -260,7 +260,7 @@ class _CalendarEventPageState extends State { ), if (Platform.isAndroid) ListTile( - leading: Text( + leading: const Text( 'Status', style: TextStyle(fontSize: 16), ), @@ -288,7 +288,7 @@ class _CalendarEventPageState extends State { value: _event?.allDay ?? false, onChanged: (value) => setState(() => _event?.allDay = value), - title: Text('All Day'), + title: const Text('All Day'), ), Padding( padding: const EdgeInsets.all(10.0), @@ -373,7 +373,7 @@ class _CalendarEventPageState extends State { padding: const EdgeInsets.all(10.0), child: TextFormField( initialValue: _event?.end?.location.name, - decoration: InputDecoration( + decoration: const InputDecoration( labelText: 'End date time zone', hintText: 'Australia/Sydney'), onSaved: (String? value) => @@ -387,18 +387,16 @@ class _CalendarEventPageState extends State { context, MaterialPageRoute( builder: (context) => - EventAttendeePage())); + const EventAttendeePage())); if (result != null) { - if (_attendees == null) { - _attendees = []; - } + _attendees ??= []; setState(() { _attendees?.add(result); }); } } : null, - leading: Icon(Icons.people), + leading: const Icon(Icons.people), title: Text(_calendar.isReadOnly == false ? 'Add Attendees' : 'Attendees'), @@ -476,7 +474,7 @@ class _CalendarEventPageState extends State { decoration: BoxDecoration( border: Border.all( color: Colors.blueAccent)), - child: Text('current user'))), + child: const Text('current user'))), Visibility( visible: _attendees?[index].isOrganiser ?? false, @@ -487,7 +485,7 @@ class _CalendarEventPageState extends State { decoration: BoxDecoration( border: Border.all( color: Colors.blueAccent)), - child: Text('Organiser'))), + child: const Text('Organiser'))), Container( margin: const EdgeInsets.symmetric( vertical: 10.0), @@ -505,7 +503,7 @@ class _CalendarEventPageState extends State { _attendees?.removeAt(index); }); }, - icon: Icon( + icon: const Icon( Icons.remove_circle, color: Colors.redAccent, ), @@ -748,10 +746,8 @@ class _CalendarEventPageState extends State { _rrule?.frequency) .data != null - ? Text(_recurrenceFrequencyToText( - _rrule?.frequency) - .data! + - ' on the ') + ? Text( + '${_recurrenceFrequencyToText(_rrule?.frequency).data!} on the ') : const Text('')), ), Padding( diff --git a/example/lib/presentation/pages/calendar_events.dart b/example/lib/presentation/pages/calendar_events.dart index 7255029f..d37b5bbc 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,9 +131,9 @@ class _CalendarEventsPageState extends State { } Future _retrieveCalendarEvents() async { - final startDate = DateTime.now().add(Duration(days: -30)); + final startDate = DateTime.now().add(const Duration(days: -30)); // final endDate = DateTime.now().add(Duration(days: 365 * 2)); - final endDate = DateTime.now().add(Duration(days: 365 * 10)); + final endDate = DateTime.now().add(const Duration(days: 365 * 10)); var calendarEventsResult = await _deviceCalendarPlugin.retrieveEvents( _calendar.id, RetrieveEventsParams(startDate: startDate, endDate: endDate)); @@ -145,7 +145,7 @@ class _CalendarEventsPageState extends State { Widget _getDeleteButton() { return IconButton( - icon: Icon(Icons.delete), + icon: const Icon(Icons.delete), onPressed: () async { await _showDeleteDialog(); }); @@ -156,10 +156,10 @@ class _CalendarEventsPageState extends State { context: context, builder: (BuildContext context) { return AlertDialog( - title: Text('Warning'), + title: const Text('Warning'), content: SingleChildScrollView( child: ListBody( - children: [ + children: const [ Text('This will delete this calendar'), Text('Are you sure?'), ], @@ -175,13 +175,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 1113480d..bbd62c32 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -6,7 +6,7 @@ import 'package:flutter/services.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 de9644a9..7936812c 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 5dad60cd..36558716 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 709aec2a..4c1d12f1 100644 --- a/lib/src/device_calendar.dart +++ b/lib/src/device_calendar.dart @@ -232,7 +232,7 @@ class DeviceCalendarPlugin { event.end = Platform.isAndroid ? TZDateTime.utc(event.end!.year, event.end!.month, event.end!.day, 0, 0, 0) - .add(Duration(days: 1)) + .add(const Duration(days: 1)) : TZDateTime.from(dateEnd, timeZoneDatabase.locations[event.end!.location.name]!); } diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index 93529c38..6c2b5697 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -140,7 +140,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']); @@ -216,7 +216,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 acc26441..6ef40a38 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 a8a217e7a5de2e5e0a238a5c9e27955d3430ec1e Mon Sep 17 00:00:00 2001 From: thomassth Date: Thu, 6 Oct 2022 21:21:22 -0400 Subject: [PATCH 06/45] 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 a18613a6..986e15f9 100644 --- a/example/lib/presentation/event_item.dart +++ b/example/lib/presentation/event_item.dart @@ -28,7 +28,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 d37b5bbc..d110ffdb 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 bbd62c32..71c47ea5 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 02859dc563ab03bc7c85acdbea9668fead8aedc2 Mon Sep 17 00:00:00 2001 From: Thomas Kam Date: Thu, 6 Oct 2022 23:58:59 -0400 Subject: [PATCH 07/45] 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 aa31c46cd694cf468dbb983bb9bbc9d75315cd50 Mon Sep 17 00:00:00 2001 From: thomassth Date: Wed, 12 Oct 2022 01:31:00 -0400 Subject: [PATCH 08/45] packages cleanup --- example/lib/presentation/event_item.dart | 1 - example/lib/presentation/pages/calendar_event.dart | 1 - example/pubspec.yaml | 1 - lib/device_calendar.dart | 1 + lib/src/models/event.dart | 2 -- pubspec.yaml | 3 --- 6 files changed, 1 insertion(+), 8 deletions(-) diff --git a/example/lib/presentation/event_item.dart b/example/lib/presentation/event_item.dart index 986e15f9..f91bb7de 100644 --- a/example/lib/presentation/event_item.dart +++ b/example/lib/presentation/event_item.dart @@ -4,7 +4,6 @@ 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 'package:timezone/timezone.dart'; import 'recurring_event_dialog.dart'; diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 1faa8dbb..750d7236 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/example/pubspec.yaml b/example/pubspec.yaml index 36558716..b32d2fb0 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -12,7 +12,6 @@ dependencies: sdk: flutter intl: ^0.17.0 uuid: ^3.0.6 - timezone: ^0.9.0 flutter_native_timezone: ^2.0.0 device_calendar: path: ../ diff --git a/lib/device_calendar.dart b/lib/device_calendar.dart index b25b1f29..3566d5df 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 6c2b5697..94ef6217 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:timezone/timezone.dart'; import '../../device_calendar.dart'; import '../common/error_messages.dart'; @@ -83,7 +82,6 @@ class Event { if (json == null) { throw ArgumentError(ErrorMessages.fromJsonMapIsNull); } - String? foundUrl; String? startLocationName; String? endLocationName; diff --git a/pubspec.yaml b/pubspec.yaml index 42aae1ff..140720ba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,10 +7,7 @@ dependencies: flutter: sdk: flutter collection: ^1.16.0 - sprintf: ^6.0.2 timezone: ^0.9.0 - flutter_native_timezone: ^2.0.0 - intl: ^0.17.0 rrule: ^0.2.7 dev_dependencies: From 53ac92fad21398daa4a231cbefb51e6f34aa3a19 Mon Sep 17 00:00:00 2001 From: Oleksandra Fedotova Date: Thu, 13 Oct 2022 12:11:52 +0300 Subject: [PATCH 09/45] 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 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index b39ee5da..fdad5ecc 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -815,13 +815,13 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele 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 From bd290e8a0cd465099b12cc04f3bfb244706b801b Mon Sep 17 00:00:00 2001 From: Oleksandra Fedotova Date: Mon, 17 Oct 2022 12:50:37 +0300 Subject: [PATCH 10/45] Fixed formatting --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index fdad5ecc..cb102cd2 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -815,12 +815,12 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele ekEvent!.notes = description ekEvent!.isAllDay = isAllDay ekEvent!.startDate = startDate - ekEvent!.endDate = endDate + ekEvent!.endDate = endDate if (!isAllDay) { - let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current + let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current ekEvent!.timeZone = timeZone - } + } ekEvent!.calendar = ekCalendar! ekEvent!.location = location From d3d39271e8f99dafefa3cf3abd45070a84f9e22d Mon Sep 17 00:00:00 2001 From: Oleksandra Fedotova Date: Mon, 17 Oct 2022 19:33:08 +0300 Subject: [PATCH 11/45] Fixed file formatting with spaces --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index cb102cd2..ed8d6fb7 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -440,13 +440,13 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele private func convertEkEventStatus(ekEventStatus: EKEventStatus?) -> EventStatus? { switch ekEventStatus { case .confirmed: - return EventStatus.CONFIRMED + return EventStatus.CONFIRMED case .tentative: return EventStatus.TENTATIVE - case .canceled: - return EventStatus.CANCELED - case .none?: - return EventStatus.NONE + case .canceled: + return EventStatus.CANCELED + case .none?: + return EventStatus.NONE default: return nil } @@ -817,7 +817,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele ekEvent!.startDate = startDate ekEvent!.endDate = endDate - if (!isAllDay) { + if (!isAllDay) { let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current ekEvent!.timeZone = timeZone } From 5ba6b55409bc5c007511642f06ebf4f0509950fc Mon Sep 17 00:00:00 2001 From: "goldensoju@gmailcom" Date: Tue, 1 Nov 2022 18:28:07 +0900 Subject: [PATCH 12/45] iOS: Fix error when adding attendees due to UUID is nil. --- example/ios/Podfile.lock | 6 +++--- example/lib/presentation/pages/calendar_event.dart | 5 ++++- example/lib/presentation/pages/event_attendee.dart | 8 ++++---- ios/Classes/SwiftDeviceCalendarPlugin.swift | 1 + pubspec.yaml | 2 +- test/device_calendar_test.dart | 1 - 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index f58f9b39..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.11.2 +COCOAPODS: 1.11.3 diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 750d7236..431b9b03 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -407,7 +407,10 @@ class _CalendarEventPageState extends State { itemBuilder: (context, index) { return Container( color: (_attendees?[index].isOrganiser ?? false) - ? Colors.greenAccent[100] + ? 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 7936812c..b018d8b6 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 b39ee5da..c0c724b2 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -856,6 +856,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/pubspec.yaml b/pubspec.yaml index 140720ba..014d8bcb 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.7 + rrule: ^0.2.10 dev_dependencies: flutter_test: diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart index 6ef40a38..ada9d769 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 05dc04d8ab2f4ac7d59908df519fa9bff36b3566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20J=C3=A4rvstrand?= Date: Sat, 31 Dec 2022 13:23:10 +0100 Subject: [PATCH 13/45] Remove faulty requestPermissions implementation in favor of working one --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index f20bd09b..fb6c446a 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -1024,7 +1024,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele self.finishWithUnauthorizedError(result: result) } - private func requestPermissions(completion: @escaping (Bool) -> Void) { + private func requestPermissions(_ completion: @escaping (Bool) -> Void) { if hasEventPermissions() { completion(true) return @@ -1039,16 +1039,6 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let status = EKEventStore.authorizationStatus(for: .event) return status == EKAuthorizationStatus.authorized } - - private func requestPermissions(_ result: @escaping FlutterResult) { - if hasEventPermissions() { - result(true) - } - eventStore.requestAccess(to: .event, completion: { - (accessGranted: Bool, _: Error?) in - result(accessGranted) - }) - } } extension Date { From 0c85ab0d158763ca0f6c93f4a8d88b5c57b87c7f Mon Sep 17 00:00:00 2001 From: Shreyas S Date: Wed, 15 Feb 2023 11:45:23 +0530 Subject: [PATCH 14/45] 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 15/45] 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 16/45] 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 17/45] 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 18/45] 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 19/45] 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 20/45] 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 21/45] 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 22/45] 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 23/45] 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 24/45] 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 25/45] 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 26/45] 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 27/45] 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 28/45] 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 29/45] 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 30/45] 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 31/45] 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 32/45] 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 33/45] 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 34/45] 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 35/45] 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 36/45] 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 37/45] 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 38/45] 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 39/45] 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 40/45] 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 41/45] 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 42/45] 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 43/45] 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 44/45] 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 45/45] 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: