diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 25401783..744c0ba4 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -34,8 +34,9 @@ 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 - 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 }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bb5d1ed..2fa175fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,17 @@ -## [4.2.1](https://github.com/builttoroam/device_calendar/releases/tag/4.2.1) +## [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 - *Note:* `timezone 0.9.0` [removed named database files](https://pub.dev/packages/timezone/changelog#090). If you are only using `device_calendar`, you can ignore this note. +- Added support for all-day multi-day events on iOS +- Fixed iOS issue of adding attendees to events +- Fixed Android issue of the `ownerAccount` being null ## [4.2.0](https://github.com/builttoroam/device_calendar/releases/tag/4.2.0) diff --git a/README.md b/README.md index a91c6e5c..2af1e99d 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()`. @@ -107,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 66d86743..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() @@ -25,14 +25,15 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 30 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - minSdkVersion 16 + minSdkVersion 19 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'proguard-rules.pro' } lintOptions { disable 'InvalidPackage' @@ -45,6 +46,7 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } + namespace 'com.builttoroam.devicecalendar' } dependencies { 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/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt index 1cd3f98a..dd73716f 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,8 @@ 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) + values.put(Events.EVENT_COLOR, event.eventColor) return values } @@ -938,6 +942,8 @@ 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 eventColorKey = cursor.getInt(Cst.EVENT_PROJECTION_EVENT_COLOR_KEY_INDEX) val event = Event() event.eventTitle = title ?: "New Event" event.eventId = eventId.toString() @@ -953,6 +959,8 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : event.eventEndTimeZone = endTimeZone event.availability = availability event.eventStatus = eventStatus + event.eventColor = if (eventColor == 0) null else eventColor + event.eventColorKey = if (eventColorKey == 0) null else eventColorKey return event } @@ -1125,6 +1133,73 @@ 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 + **/ + private fun retrieveColors(accountName: String, colorType: Int): 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(colorType.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() + } + + 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 + */ + 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..a5d7df80 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt @@ -25,10 +25,14 @@ 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" +private const val RETRIEVE_CALENDAR_COLORS_METHOD = "retrieveCalendarColors" +private const val UPDATE_CALENDAR_COLOR = "updateCalendarColor" // 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 +70,8 @@ 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" +private const val CALENDAR_COLOR_KEY_ARGUMENT = "calendarColorKey" class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { @@ -171,6 +177,35 @@ 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) + 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) { + result.success(intArrayOf()) + 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() } @@ -192,6 +227,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..f02eebd2 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,8 @@ 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 + const val EVENT_PROJECTION_EVENT_COLOR_KEY_INDEX: Int = 16 val EVENT_PROJECTION: Array = arrayOf( CalendarContract.Instances.EVENT_ID, @@ -66,7 +68,9 @@ class Constants { CalendarContract.Events.EVENT_TIMEZONE, CalendarContract.Events.EVENT_END_TIMEZONE, CalendarContract.Events.AVAILABILITY, - CalendarContract.Events.STATUS + CalendarContract.Events.STATUS, + CalendarContract.Events.EVENT_COLOR, + CalendarContract.Events.EVENT_COLOR_KEY ) const val EVENT_INSTANCE_DELETION_ID_INDEX: Int = 0 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 c7e3f6bf..6e10b7fe 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt @@ -1,6 +1,13 @@ package com.builttoroam.devicecalendar.models -class Calendar(val id: String, val name: String, val color : Int, val accountName: String, val accountType: String, val ownerAccount: String?) { +class Calendar( + val id: String, + val name: String, + val color: Int, + val accountName: String, + val accountType: String, + val ownerAccount: String? +) { var isReadOnly: Boolean = false var isDefault: Boolean = false -} \ No newline at end of file +} diff --git a/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/analysis_options.yaml b/example/analysis_options.yaml index 68a79339..3e1200f9 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -27,4 +27,4 @@ linter: # 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 +# https://dart.dev/guides/language/analysis-options diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index dd924715..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 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 31 versionCode 1 versionName "1.0" diff --git a/example/android/build.gradle b/example/android/build.gradle index d135914c..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() @@ -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 +} 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 + 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/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/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 431b9b03..83ad1a23 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -4,9 +4,10 @@ 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'; import '../date_time_picker.dart'; import '../recurring_event_dialog.dart'; import 'event_attendee.dart'; @@ -18,14 +19,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,16 +63,17 @@ 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(); } void getCurentLocation() async { try { - _timezone = await FlutterNativeTimezone.getLocalTimezone(); + _timezone = await FlutterTimezone.getLocalTimezone(); } catch (e) { debugPrint('Could not get the local timezone'); } @@ -283,42 +286,66 @@ 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 { + if (_eventColors != null) { + final colors = _eventColors?.map((eventColor) => Color(eventColor.color)).toList(); + final newColor = await ColorPickerDialog.selectColorDialog(colors ?? [], context); + setState(() { + _event?.updateEventColor(_eventColors?.firstWhereOrNull((eventColor) => Color(eventColor.color).value == newColor?.value)); + }); + } + }, + ), SwitchListTile( value: _event?.allDay ?? false, onChanged: (value) => setState(() => _event?.allDay = value), title: const Text('All Day'), ), - Padding( - padding: const EdgeInsets.all(10.0), - child: DateTimePicker( - labelText: 'From', - enableTime: _event?.allDay == false, - selectedDate: _startDate, - selectedTime: _startTime, - selectDate: (DateTime date) { - setState(() { - var currentLocation = - timeZoneDatabase.locations[_timezone]; - if (currentLocation != null) { - _startDate = - TZDateTime.from(date, currentLocation); - _event?.start = _combineDateWithTime( - _startDate, _startTime); - } - }); - }, - selectTime: (TimeOfDay time) { - setState( - () { - _startTime = time; - _event?.start = _combineDateWithTime( - _startDate, _startTime); - }, - ); - }, + if (_startDate != null) + Padding( + padding: const EdgeInsets.all(10.0), + child: DateTimePicker( + labelText: 'From', + enableTime: _event?.allDay == false, + selectedDate: _startDate, + selectedTime: _startTime, + selectDate: (DateTime date) { + setState(() { + var currentLocation = + timeZoneDatabase.locations[_timezone]; + if (currentLocation != null) { + _startDate = + TZDateTime.from(date, currentLocation); + _event?.start = _combineDateWithTime( + _startDate, _startTime); + } + }); + }, + selectTime: (TimeOfDay time) { + setState( + () { + _startTime = time; + _event?.start = _combineDateWithTime( + _startDate, _startTime); + }, + ); + }, + ), ), - ), if ((_event?.allDay == false) && Platform.isAndroid) Padding( padding: const EdgeInsets.all(10.0), @@ -403,14 +430,14 @@ class _CalendarEventPageState extends State { ListView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, - itemCount: (_attendees ?? []).length, + itemCount: _attendees?.length ?? 0, itemBuilder: (context, index) { return Container( color: (_attendees?[index].isOrganiser ?? false) ? MediaQuery.of(context).platformBrightness == - Brightness.dark - ? Colors.black26 - : Colors.greenAccent[100] + Brightness.dark + ? Colors.black26 + : Colors.greenAccent[100] : Colors.transparent, child: ListTile( onTap: () async { @@ -537,7 +564,7 @@ class _CalendarEventPageState extends State { spacing: 10.0, children: [ const Icon(Icons.alarm), - if (_reminders?.isEmpty ?? false) + if (_reminders?.isEmpty ?? true) Text(_calendar.isReadOnly == false ? 'Add reminders' : 'Reminders'), @@ -673,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)]); } }); }, @@ -693,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); }); } @@ -721,7 +748,7 @@ class _CalendarEventPageState extends State { if (value != null) { setState(() { _rrule = - _rrule?.copyWith(byMonthDays: {value}); + _rrule?.copyWith(byMonthDays: [value]); }); } }, @@ -765,10 +792,10 @@ class _CalendarEventPageState extends State { _rrule?.byWeekDays.first.day ?? 1; setState(() { _rrule = _rrule?.copyWith( - byWeekDays: { + byWeekDays: [ ByWeekDayEntry( weekDay, value.index + 1) - }); + ]); }); } }, @@ -794,10 +821,10 @@ class _CalendarEventPageState extends State { 1; setState(() { _rrule = _rrule?.copyWith( - byWeekDays: { + byWeekDays: [ ByWeekDayEntry( value.index + 1, weekNo) - }); + ]); }); } }, @@ -824,7 +851,7 @@ class _CalendarEventPageState extends State { if (value != null) { setState(() { _rrule = _rrule?.copyWith( - byMonths: {value.index + 1}); + byMonths: [value.index + 1]); }); } }, @@ -973,6 +1000,7 @@ class _CalendarEventPageState extends State { AutovalidateMode.always; // Start validating on every change. showInSnackBar( context, 'Please fix the errors in red before submitting.'); + return; } else { form?.save(); _adjustStartEnd(); @@ -1066,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), @@ -1089,7 +1117,7 @@ class _CalendarEventPageState extends State { ByWeekDayEntry(5), ByWeekDayEntry(6), ByWeekDayEntry(7), - }); + ]); break; case DayOfWeekGroup.None: default: @@ -1136,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; @@ -1166,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); } @@ -1175,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); } diff --git a/example/lib/presentation/pages/calendar_events.dart b/example/lib/presentation/pages/calendar_events.dart index d110ffdb..6b2e8384 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(); } @@ -77,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(); @@ -123,6 +125,7 @@ class _CalendarEventsPageState extends State { _onLoading, _onDeletedFinished, ), + _eventColors ); })); if (refreshEvents != null && refreshEvents) { @@ -132,7 +135,6 @@ class _CalendarEventsPageState extends State { Future _retrieveCalendarEvents() async { final startDate = DateTime.now().add(const Duration(days: -30)); - // final endDate = DateTime.now().add(Duration(days: 365 * 2)); final endDate = DateTime.now().add(const Duration(days: 365 * 10)); var calendarEventsResult = await _deviceCalendarPlugin.retrieveEvents( _calendar.id, @@ -143,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/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index 71c47ea5..bc434173 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/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.headline6, + style: Theme + .of(context) + .textTheme + .titleLarge, ), ), Expanded( @@ -56,14 +64,14 @@ class _CalendarsPageState extends State { 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)}'), + ? '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) { - 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 +83,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.subtitle1, + 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 +167,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 +209,4 @@ class _CalendarsPageState extends State { _retrieveCalendars(); }); } -} +} \ No newline at end of file diff --git a/example/lib/presentation/pages/event_attendee.dart b/example/lib/presentation/pages/event_attendee.dart index b018d8b6..2fff734e 100644 --- a/example/lib/presentation/pages/event_attendee.dart +++ b/example/lib/presentation/pages/event_attendee.dart @@ -112,7 +112,7 @@ class _EventAttendeePageState extends State { onTap: () async { _deviceCalendarPlugin = DeviceCalendarPlugin(); - var result = await _deviceCalendarPlugin + await _deviceCalendarPlugin .showiOSEventModal(_eventId); Navigator.popUntil( context, ModalRoute.withName(AppRoutes.calendars)); 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: ../ diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index fb6c446a..8c302f49 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" @@ -157,43 +158,48 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele var flutterResult : FlutterResult? public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: channelName, binaryMessenger: registrar.messenger()) - let instance = SwiftDeviceCalendarPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } + let channel = FlutterMethodChannel(name: channelName, binaryMessenger: registrar.messenger()) + let instance = SwiftDeviceCalendarPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } - 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) - } + 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) + case showEventModalMethod: + self.flutterResult = result + showEventModal(call, result) + case updateCalendarColor: + updateCalendarColor(call, result) + default: + result(FlutterMethodNotImplemented) } + } - private func hasPermissions(_ result: FlutterResult) { - let hasPermissions = hasEventPermissions() - result(hasPermissions) - } + private func hasPermissions(_ result: FlutterResult) { + let hasPermissions = hasEventPermissions() + result(hasPermissions) + } - private func getSource() -> EKSource? { - let localSources = eventStore.sources.filter { $0.sourceType == .local } + private func getSource() -> EKSource? { + let localSources = eventStore.sources.filter { $0.sourceType == .local } if (!localSources.isEmpty) { return localSources.first @@ -212,147 +218,203 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele return nil } - 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 + 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 (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 - } + 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)) - } + try eventStore.saveCalendar(calendar, commit: true) + result(calendar.calendarIdentifier) } + catch { + eventStore.reset() + 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) - 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) - } + 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 - self.encodeJsonAndFinish(codable: calendars, result: result) - }, result: result) + guard let calendar = eventStore.calendar(withIdentifier: calendarId) else { + print("Calendar not found") + result(false) + return } - private func deleteCalendar(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - checkPermissionsThenExecute(permissionsGrantedAction: { - let arguments = call.arguments as! Dictionary - let calendarId = arguments[calendarIdArgument] as! String + // Update the calendar color + calendar.cgColor = UIColorFromRGB(color).cgColor - let ekCalendar = self.eventStore.calendar(withIdentifier: calendarId) - if ekCalendar == nil { - self.finishWithCalendarNotFoundError(result: result, calendarId: calendarId) - return - } + // Save the changes + do { + try eventStore.saveCalendar(calendar, commit: true) + result(true) // Assuming the operation was successful, return true + } catch { + result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) + } + } - if !(ekCalendar!.allowsContentModifications) { - self.finishWithCalendarReadOnlyError(result: result, calendarId: calendarId) - return - } + 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) + ) + } - 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 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) + } - 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"; + self.encodeJsonAndFinish(codable: calendars, result: result) + }, result: result) + } + + private func deleteCalendar(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + checkPermissionsThenExecute(permissionsGrantedAction: { + let arguments = call.arguments as! Dictionary + let calendarId = arguments[calendarIdArgument] as! String + + let ekCalendar = self.eventStore.calendar(withIdentifier: calendarId) + if ekCalendar == nil { + self.finishWithCalendarNotFoundError(result: result, calendarId: calendarId) + return } + + if !(ekCalendar!.allowsContentModifications) { + self.finishWithCalendarReadOnlyError(result: result, calendarId: calendarId) + return + } + + do { + try self.eventStore.removeCalendar(ekCalendar!, commit: true) + result(true) + } 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"; } + } - 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) + 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 { + 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(fourYearsTimeInterval) + while currentEndDate <= endDate { + 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 [fourYearsTimeInterval] + currentStartDate = currentEndDate + currentEndDate = currentStartDate.addingTimeInterval(fourYearsTimeInterval) + } + + // If the cycle doesn't end exactly on the end date + if currentStartDate <= endDate { + 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) } } + } - guard let eventIds = eventIdArgs else { - self.encodeJsonAndFinish(codable: events, result: result) - return - } + 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) - }) + if specifiedStartEndDates { + events = events.filter({ (e) -> Bool in + e.calendarId == calendarId && eventIds.contains(e.eventId) + }) - self.encodeJsonAndFinish(codable: events, result: result) - return - } + self.encodeJsonAndFinish(codable: events, result: result) + return + } - for eventId in eventIds { - let ekEvent = self.eventStore.event(withIdentifier: eventId) - if ekEvent == nil { - continue - } + for eventId in eventIds { + let ekEvent = self.eventStore.event(withIdentifier: eventId) + if ekEvent == nil { + continue + } let event = createEventFromEkEvent(calendarId: calendarId, ekEvent: ekEvent!) @@ -406,21 +468,21 @@ 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 - ) + let attendee = Attendee( + name: ekParticipant!.name, + emailAddress: ekParticipant!.emailAddress!, + role: ekParticipant!.participantRole.rawValue, + attendanceStatus: ekParticipant!.participantStatus.rawValue, + isCurrentUser: ekParticipant!.isCurrentUser + ) - return attendee - } + return attendee + } private func convertEkEventAvailability(ekEventAvailability: EKEventAvailability?) -> Availability? { switch ekEventAvailability { @@ -779,13 +841,13 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let arguments = call.arguments as! Dictionary let calendarId = arguments[calendarIdArgument] as! String let eventId = arguments[eventIdArgument] as? String - let isAllDay = arguments[eventAllDayArgument] as! Bool + let isAllDay = (arguments[eventAllDayArgument] as? Bool) ?? false let startDateMillisecondsSinceEpoch = arguments[eventStartDateArgument] as! NSNumber let endDateDateMillisecondsSinceEpoch = arguments[eventEndDateArgument] as! NSNumber let startDate = Date (timeIntervalSince1970: startDateMillisecondsSinceEpoch.doubleValue / 1000.0) let endDate = Date (timeIntervalSince1970: endDateDateMillisecondsSinceEpoch.doubleValue / 1000.0) let startTimeZoneString = arguments[eventStartTimeZoneArgument] as? String - let title = arguments[self.eventTitleArgument] as! String + let title = arguments[self.eventTitleArgument] as? String let description = arguments[self.eventDescriptionArgument] as? String let location = arguments[self.eventLocationArgument] as? String let url = arguments[self.eventURLArgument] as? String @@ -811,17 +873,17 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } } - ekEvent!.title = title + ekEvent!.title = title ?? "" ekEvent!.notes = description ekEvent!.isAllDay = isAllDay ekEvent!.startDate = startDate ekEvent!.endDate = endDate - - if (!isAllDay) { + + if (!isAllDay) { let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current ekEvent!.timeZone = timeZone } - + ekEvent!.calendar = ekCalendar! ekEvent!.location = location @@ -1029,15 +1091,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 + } } } diff --git a/lib/device_calendar.dart b/lib/device_calendar.dart index 3566d5df..ab9f78d6 100644 --- a/lib/device_calendar.dart +++ b/lib/device_calendar.dart @@ -6,6 +6,8 @@ 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/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 2eef3d2d..b56f8adf 100644 --- a/lib/src/common/channel_constants.dart +++ b/lib/src/common/channel_constants.dart @@ -11,6 +11,9 @@ 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 methodNameRetrieveCalendarColors = 'retrieveCalendarColors'; + static const String methodNameUpdateCalendarColor = 'updateCalendarColor'; static const String parameterNameCalendarId = 'calendarId'; static const String parameterNameStartDate = 'startDate'; @@ -22,5 +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 4c1d12f1..6fc778e9 100644 --- a/lib/src/device_calendar.dart +++ b/lib/src/device_calendar.dart @@ -2,18 +2,14 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:io'; +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 { @@ -342,6 +338,80 @@ 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(); + } + + /// 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( String channelMethodName, { Function(Result)? assertParameters, 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 diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index 94ef6217..eda68ffa 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,12 @@ class Event { /// Indicates if this event is of confirmed, canceled, tentative or none status EventStatus? status; + /// Read-only. Android exclusive. Updatable only using [Event.updateEventColor] with color from [DeviceCalendarPlugin.retrieveEventColors] + int? color; + + /// Read-only. Android exclusive. Updatable only using [Event.updateEventColor] with color from [DeviceCalendarPlugin.retrieveEventColors] + int? colorKey; + ///Note for development: /// ///JSON field names are coded in dart, swift and kotlin to facilitate data exchange. @@ -110,6 +117,8 @@ class Event { calendarId = json['calendarId']; title = json['eventTitle']; description = json['eventDescription']; + color = json['eventColor']; + colorKey = json['eventColorKey']; startTimestamp = json['eventStartDate']; startLocationName = json['eventStartTimeZone']; @@ -237,6 +246,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 +321,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 diff --git a/pubspec.yaml b/pubspec.yaml index 014d8bcb..101681fb 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.2.1 +version: 4.3.1 homepage: https://github.com/builttoroam/device_calendar/tree/master dependencies: @@ -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: diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart index ada9d769..0d91e738 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; @@ -211,6 +217,7 @@ void main() { reminders: [reminder], availability: Availability.Busy, status: EventStatus.Confirmed); + event.updateEventColor(EventColor(0xffff00ff, 1)); final stringEvent = event.toJson(); expect(stringEvent, isNotNull); @@ -235,5 +242,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)); }); }