From b22ec09b3a41430759e5b1aa8b11199c6bb248e5 Mon Sep 17 00:00:00 2001 From: Kiet Vo A Date: Sun, 1 Jan 2023 01:34:13 +0700 Subject: [PATCH 1/2] Fix issue on macOS --- .DS_Store | Bin 10244 -> 10244 bytes lib/src/models/attendee.dart | 2 +- macos/Classes/DeviceCalendarPlugin.swift | 1053 ++++++++++++++++++++++ macos/device_calendar.podspec | 20 + pubspec.yaml | 4 +- 5 files changed, 1077 insertions(+), 2 deletions(-) create mode 100644 macos/Classes/DeviceCalendarPlugin.swift create mode 100644 macos/device_calendar.podspec diff --git a/.DS_Store b/.DS_Store index 94eb7a16cd5047b80c643861c43868f5138bd825..84cbd7498c05ed29548fb5f0ae2a480269a1d9a6 100644 GIT binary patch delta 184 zcmZn(XbG6$&&aLBKhkgOigpUO=L{+Q`g6N5R<0vQ|f-+R_NfHZeA< zt>xqpRo1r-iqFo;&CBlsng#@nj1ZcE7fQpZZXg564Q0Vac{%xc=|EA&jfs9to7olq GvI79W2Pduo delta 28 kcmZn(XbG6$&&azmU^hP_?`9r>BRm^Z&6zi|EBs{#0EwyzJOBUy diff --git a/lib/src/models/attendee.dart b/lib/src/models/attendee.dart index 2e8e4cb9..136cef80 100644 --- a/lib/src/models/attendee.dart +++ b/lib/src/models/attendee.dart @@ -56,7 +56,7 @@ class Attendee { androidAttendeeDetails = AndroidAttendeeDetails.fromJson(json); } - if (Platform.isIOS) { + if (Platform.isIOS || Platform.isMacOS) { iosAttendeeDetails = IosAttendeeDetails.fromJson(json); } } diff --git a/macos/Classes/DeviceCalendarPlugin.swift b/macos/Classes/DeviceCalendarPlugin.swift new file mode 100644 index 00000000..23c1ea2b --- /dev/null +++ b/macos/Classes/DeviceCalendarPlugin.swift @@ -0,0 +1,1053 @@ +import EventKit +//import EventKitUI +import FlutterMacOS +import Foundation +//import UIKit + +extension Date { + var millisecondsSinceEpoch: Double { return self.timeIntervalSince1970 * 1000.0 } +} + +extension EKParticipant { + var emailAddress: String? { + return self.value(forKey: "emailAddress") as? String + } +} + +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" } + + if (!iCloudSources.isEmpty) { + return iCloudSources.first + } + + 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 + + if (calendarColor != nil) { + calendar.cgColor = NSColor(hex: calendarColor!)?.cgColor + } + else { + calendar.cgColor = NSColor(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)) + } + } + + 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: ekCalendar.color.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 { + 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) + } + } + } + + 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 { + for ekParticipant in ekEvent.attendees! { + let attendee = convertEkParticipantToAttendee(ekParticipant: ekParticipant) + 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, + calendarId: calendarId, + eventTitle: ekEvent.title ?? "New Event", + eventDescription: ekEvent.notes, + eventStartDate: Int64(ekEvent.startDate.millisecondsSinceEpoch), + eventEndDate: Int64(ekEvent.endDate.millisecondsSinceEpoch), + eventStartTimeZone: ekEvent.timeZone?.identifier, + eventAllDay: ekEvent.isAllDay, + attendees: attendees, + eventLocation: ekEvent.location, + eventURL: ekEvent.url?.absoluteString, + recurrenceRule: recurrenceRule, + organizer: convertEkParticipantToAttendee(ekParticipant: ekEvent.organizer), + reminders: reminders, + availability: convertEkEventAvailability(ekEventAvailability: ekEvent.availability), + eventStatus: convertEkEventStatus(ekEventStatus: ekEvent.status) + ) + + return event + } + + private func convertEkParticipantToAttendee(ekParticipant: EKParticipant?) -> Attendee? { + if ekParticipant == nil || ekParticipant?.emailAddress == nil { + return nil + } + + let attendee = Attendee( + name: ekParticipant!.name, + emailAddress: ekParticipant!.emailAddress!, + 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 + case .free: + return Availability.FREE + case .tentative: + return Availability.TENTATIVE + case .unavailable: + return Availability.UNAVAILABLE + default: + return nil + } + } + + private func convertEkEventStatus(ekEventStatus: EKEventStatus?) -> EventStatus? { + switch ekEventStatus { + case .confirmed: + return EventStatus.CONFIRMED + case .tentative: + return EventStatus.TENTATIVE + case .canceled: + return EventStatus.CANCELED + case .none?: + return EventStatus.NONE + default: + return nil + } + } + + private func parseEKRecurrenceRules(_ ekEvent: EKEvent) -> RecurrenceRule? { + var recurrenceRule: RecurrenceRule? + if ekEvent.hasRecurrenceRules { + let ekRecurrenceRule = ekEvent.recurrenceRules![0] + var frequency: String + switch ekRecurrenceRule.frequency { + case EKRecurrenceFrequency.daily: + frequency = "DAILY" + case EKRecurrenceFrequency.weekly: + frequency = "WEEKLY" + case EKRecurrenceFrequency.monthly: + frequency = "MONTHLY" + case EKRecurrenceFrequency.yearly: + frequency = "YEARLY" + default: + frequency = "DAILY" + } + + var count: Int? + var endDate: String? + if(ekRecurrenceRule.recurrenceEnd?.occurrenceCount != nil && ekRecurrenceRule.recurrenceEnd?.occurrenceCount != 0) { + count = ekRecurrenceRule.recurrenceEnd?.occurrenceCount + } + + let endDateRaw = ekRecurrenceRule.recurrenceEnd?.endDate + if(endDateRaw != nil) { + endDate = formateDateTime(dateTime: endDateRaw!) + } + + 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( + freq: frequency, + count: count, + interval: ekRecurrenceRule.interval, + 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 recurrenceFrequency = recurrenceRuleArguments![recurrenceFrequencyArgument] as? String + let totalOccurrences = recurrenceRuleArguments![countArgument] as? NSInteger + let interval = recurrenceRuleArguments![intervalArgument] as? NSInteger + var recurrenceInterval = 1 + 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 { + 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 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!)} + } + } + + 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: 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 + } + if existingAttendee != nil && ekEvent!.organizer?.emailAddress != existingAttendee?.emailAddress{ + attendees.append(existingAttendee!) + 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 + } + + switch availabilityValue.uppercased() { + case Availability.BUSY.rawValue: + return .busy + case Availability.FREE.rawValue: + return .free + 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 + let calendarId = arguments[calendarIdArgument] as! String + let eventId = arguments[eventIdArgument] as? String + let isAllDay = arguments[eventAllDayArgument] as! Bool + 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 description = arguments[self.eventDescriptionArgument] as? String + let location = arguments[self.eventLocationArgument] as? String + let url = arguments[self.eventURLArgument] 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 + } + + var ekEvent: EKEvent? + if eventId == nil { + ekEvent = EKEvent.init(eventStore: self.eventStore) + } else { + ekEvent = self.eventStore.event(withIdentifier: eventId!) + if(ekEvent == nil) { + self.finishWithEventNotFoundError(result: result, eventId: eventId!) + return + } + } + + ekEvent!.title = title + ekEvent!.notes = description + ekEvent!.isAllDay = isAllDay + ekEvent!.startDate = startDate + ekEvent!.endDate = endDate + + if (!isAllDay) { + let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current + ekEvent!.timeZone = timeZone + } + + ekEvent!.calendar = ekCalendar! + ekEvent!.location = location + + // Create and add URL object only when if the input string is not empty or nil + if let urlCheck = url, !urlCheck.isEmpty { + let iosUrl = URL(string: url ?? "") + ekEvent!.url = iosUrl + } + 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) + } catch { + self.eventStore.reset() + result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) + } + }, result: result) + } + + private func createParticipant(name: String, emailAddress: String, role: Int) -> EKParticipant? { + 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") + return participant as? EKParticipant + } + return nil + } + + private func deleteEvent(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + checkPermissionsThenExecute(permissionsGrantedAction: { + let arguments = call.arguments as! Dictionary + let calendarId = arguments[calendarIdArgument] as! String + let eventId = arguments[eventIdArgument] as! String + 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) + } catch { + self.eventStore.reset() + result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) + } + } + 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) + } + else { + try self.eventStore.remove(ekEvent!, span: .futureEvents, commit: true) + } + + result(true) + } catch { + self.eventStore.reset() + result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) + } + } + }, result: result) + } + + private func showEventModal(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {} + + + 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() + let jsonData = try jsonEncoder.encode(codable) + let jsonString = String(data: jsonData, encoding: .utf8) + result(jsonString) + } catch { + result(FlutterError(code: genericError, message: error.localizedDescription, details: nil)) + } + } + + private func checkPermissionsThenExecute(permissionsGrantedAction: () -> Void, result: @escaping FlutterResult) { + if hasEventPermissions() { + permissionsGrantedAction() + return + } + self.finishWithUnauthorizedError(result: result) + } + + private func requestPermissions(completion: @escaping (Bool) -> Void) { + if hasEventPermissions() { + completion(true) + return + } + 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 + } + + private func requestPermissions(_ result: @escaping FlutterResult) { + if hasEventPermissions() { + result(true) + } + eventStore.requestAccess(to: .event, completion: { + (accessGranted: Bool, _: Error?) in + result(accessGranted) + }) + } +} + +extension Date { + func convert(from initTimeZone: TimeZone, to targetTimeZone: TimeZone) -> Date { + let delta = TimeInterval(initTimeZone.secondsFromGMT() - targetTimeZone.secondsFromGMT()) + return addingTimeInterval(delta) + } +} + +extension NSColor { + func rgb() -> Int? { + + let ciColor:CIColor = CIColor(color: self)! + let fRed : CGFloat = ciColor.red + let fGreen : CGFloat = ciColor.green + let fBlue : CGFloat = ciColor.blue + let fAlpha: CGFloat = ciColor.alpha + + let iRed = Int(fRed * 255.0) + let iGreen = Int(fGreen * 255.0) + let iBlue = Int(fBlue * 255.0) + let iAlpha = Int(fAlpha * 255.0) + + // (Bits 24-31 are alpha, 16-23 are red, 8-15 are green, 0-7 are blue). + let rgb = (iAlpha << 24) + (iRed << 16) + (iGreen << 8) + iBlue + return rgb + } + + public convenience init?(hex: String) { + let r, g, b, a: CGFloat + + if hex.hasPrefix("0x") { + let start = hex.index(hex.startIndex, offsetBy: 2) + let hexColor = String(hex[start...]) + + if hexColor.count == 8 { + let scanner = Scanner(string: hexColor) + var hexNumber: UInt64 = 0 + + if scanner.scanHexInt64(&hexNumber) { + a = CGFloat((hexNumber & 0xff000000) >> 24) / 255 + 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 + } + } + } + + return nil + } +} diff --git a/macos/device_calendar.podspec b/macos/device_calendar.podspec new file mode 100644 index 00000000..9d1b9481 --- /dev/null +++ b/macos/device_calendar.podspec @@ -0,0 +1,20 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'device_calendar' + s.version = '0.0.1' + s.summary = 'A new flutter plugin project.' + s.description = <<-DESC +A new flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.15' + s.swift_version = '5.0' +end \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 014d8bcb..6a11b687 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,9 @@ flutter: pluginClass: DeviceCalendarPlugin ios: pluginClass: DeviceCalendarPlugin + macos: + pluginClass: DeviceCalendarPlugin environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" + flutter: ">=2.8.0" From df01e873f9f14c8b1d0e5b486182ca9a9dccb8d1 Mon Sep 17 00:00:00 2001 From: Kiet Vo A Date: Sun, 1 Jan 2023 01:35:34 +0700 Subject: [PATCH 2/2] Remove .DS_Store --- .DS_Store | Bin 10244 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 84cbd7498c05ed29548fb5f0ae2a480269a1d9a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10244 zcmeHMX>c4z6@G6eThk*eZaH?WS$plZh3~bj%Q}2zb@))^5SJxcvg25u-5J|c8SN-L zvyyDZlnD1_5=bglu>(cIFAU^YiX{Bt2Lw_{xI-X7r~-b#KW+;C0I$1e<<&}ZK_wNZ zsF|wi*WIt*&h(q__1CW#0I<7YH31|6K%-kozKn_mE}<9qyek&=EGLO%50;;GGoI(p z8yR&(9*8^;c_8vY=L?p_HEa2^~8O7r(BFd+vPHxT zEtgZt>88fE##FMcv+43>rmf!8w7>7{*u_iJ4_+sAF!44etO*9>fblrQ(m7kybTcPwvD(7cDU zo?95pk)}3ly4eB^w!2(+z`11mBY`~?h}tRN%LhGj;D|%EqHmP15cc1XYn<>bx}O*&R^;S~8Y7dYn} zJI`2cZ0TUm9J61$V|5)j;>Oydl^T1)R^#>BNFn1qP;i2a>KR6I1Jn4kr!9K>yFE+% zq9rwkk=&$d_lo|WyiKL*X2!PdWMB^ZG?VZ(8+I|q{lWX`gDX?k(yB3T4Y_80>bJNF;ZSS_YTvfkJj&Bs^WMt6^<@t4ksXCF2F;GS_iM~|1YOB#nY?WE9j zS(a^S_35n&4AKMj=|d!2vh~ryxrKG6|oAPr(&<0-lEJ@GN{Cz75}jm*5rn z0lWdfhTp-T;Lq?^_#6BSt58FR)mVoca3j{^F6_Wg+=KhD3-7{yJc{qd0X&WO;Tb%O z=g`A(d;oo%#t-6$@Wc2q{0v^jYxpF79>0oT!{_jM{3d=2zl%S@U#K^X74dwxY8Oj< zlWJvnCeVg)8#}WQXcLDHAL+g&pnYSRKx*xZ+gGkySHE>|#8{u$kl+G9s`;M8<^+!s##M7$ zVh7_DgkUBIH?YP8UqWD2+7`B(@k&Co(sr;t3BHtYtF-&sos5HsH>K`QY-SuawipT* z#o!m>x8WUxIs?~Y0ypDU+=e?ag}bqx5V!~TVlN)T z!`MeC9Kazwfg^YlGiafW=kbEXV1N@ii8J^Ret?kp5qy}i_*r}uAH&b#(`DG}E22(G z{PKM4O$_97u9rJc8iCjfq8JbS5+-@Ih{#*TBx{IC+B;fO$(_v|9g0iZJ5tHcw$^!E zaxI)^G4{&2xNnx zxW$xNTf1%)R|wu8cJAG$8?#Fd#eUeG?lO$Bg@+ab8NM~f`u~nRn2g&7%A}6KY1&zg z!$snbEAVN!3SWe0h&#SP%<(FGpQ87N@N6@G8Z9XG&w6VA}9;|C8NzC~bthe%_Mo+!e+9R7+-Gyxl4C3zs8jBgZ_!PQD1 z-xS{>D1*nOR^J|{k*3Bg)ZkrulSc65OO&=%ZxfWkE0wlW->VT$`BJ4lpr?s4>WDd1 zdsCctz>87F;@EqNb}cW^p5+z7-0Scw_$~Yq{)tF?m0F3nHN+X~a0_w9cH)ek*hZY8 z_ACd9Gma2v^b_ueaF{sb6rs7F8k-yfqJ0DD$K^|3A3+ z|Nn38A(3_Dfye{zY7by#??7)iy<#zeDSumW)}EyMEZyP~<7SkUCRB(aqF)}zQ$rlb r%QF<}3r|m^J{^vmQBs