forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add Apple Calendar and Notes export integration for action items #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import Foundation | ||
| import EventKit | ||
| import UIKit | ||
| import Flutter | ||
|
|
||
| /// Native service for handling Apple Calendar interactions via Flutter. | ||
| /// | ||
| /// Exposes two methods to Flutter: `createEvent` to add a new event to the | ||
| /// user's default calendar and `checkAvailability` to verify that Calendar | ||
| /// services are available. All calls are dispatched on the main thread to | ||
| /// ensure proper UI interactions and permission prompts. | ||
| class AppleCalendarService: NSObject { | ||
| private let eventStore = EKEventStore() | ||
|
|
||
| func handleMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) { | ||
| switch call.method { | ||
| case "createEvent": | ||
| createEvent(call: call, result: result) | ||
| case "checkAvailability": | ||
| checkAvailability(result: result) | ||
| default: | ||
| result(FlutterMethodNotImplemented) | ||
| } | ||
| } | ||
|
|
||
| private func createEvent(call: FlutterMethodCall, result: @escaping FlutterResult) { | ||
| guard let args = call.arguments as? [String: Any], | ||
| let title = args["title"] as? String, | ||
| let notes = args["notes"] as? String else { | ||
| result(FlutterError(code: "INVALID_ARGUMENTS", | ||
| message: "Invalid arguments for createEvent", | ||
| details: nil)) | ||
| return | ||
| } | ||
|
|
||
| // Optional: allow custom duration from args, default to 1 hour | ||
| let duration = (args["durationMinutes"] as? Double) ?? 60.0 | ||
|
|
||
| // Request calendar permissions and then create the event | ||
| if #available(iOS 17.0, *) { | ||
| eventStore.requestFullAccessToEvents { granted, _ in | ||
| DispatchQueue.main.async { | ||
| if granted { | ||
| self.createAndSaveEvent(title: title, notes: notes, duration: duration, result: result) | ||
| } else { | ||
| result([ | ||
| "success": false, | ||
| "message": "Calendar access denied. Please enable in Settings." | ||
| ]) | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
| eventStore.requestAccess(to: .event) { granted, _ in | ||
| DispatchQueue.main.async { | ||
| if granted { | ||
| self.createAndSaveEvent(title: title, notes: notes, duration: duration, result: result) | ||
| } else { | ||
| result([ | ||
| "success": false, | ||
| "message": "Calendar access denied. Please enable in Settings." | ||
| ]) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private func createAndSaveEvent(title: String, notes: String, duration: Double, result: @escaping FlutterResult) { | ||
| do { | ||
| let event = EKEvent(eventStore: self.eventStore) | ||
| event.title = title | ||
| event.notes = notes | ||
| // Event duration: now + specified minutes (default 60) | ||
| event.startDate = Date() | ||
| event.endDate = Date().addingTimeInterval(duration * 60) | ||
| event.calendar = self.eventStore.defaultCalendarForNewEvents | ||
| try self.eventStore.save(event, span: .thisEvent) | ||
| result([ | ||
| "success": true, | ||
| "message": "Event created in Calendar" | ||
| ]) | ||
| } catch { | ||
| result([ | ||
| "success": false, | ||
| "message": "Failed to create event: \(error.localizedDescription)" | ||
| ]) | ||
| } | ||
| } | ||
|
|
||
| private func checkAvailability(result: @escaping FlutterResult) { | ||
| // Calendar services are always available on iOS | ||
| result(true) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import Foundation | ||
| import UIKit | ||
| import Flutter | ||
|
|
||
| /// Native service for interacting with Apple Notes via Flutter. | ||
| /// | ||
| /// Handles two methods invoked from Dart: `shareToNotes` to present the | ||
| /// native share sheet pre‑filled with the action item content, and | ||
| /// `isNotesAppAvailable` to determine if the Notes app is installed on the | ||
| /// device. All UI operations occur on the main thread. | ||
| class AppleNotesService { | ||
| func handleMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) { | ||
| switch call.method { | ||
| case "shareToNotes": | ||
| shareToNotes(call: call, result: result) | ||
| case "isNotesAppAvailable": | ||
| isNotesAppAvailable(result: result) | ||
| default: | ||
| result(FlutterMethodNotImplemented) | ||
| } | ||
| } | ||
|
|
||
| private func shareToNotes(call: FlutterMethodCall, result: @escaping FlutterResult) { | ||
| guard let args = call.arguments as? [String: Any], | ||
| let content = args["content"] as? String else { | ||
| result(FlutterError(code: "INVALID_ARGUMENTS", | ||
| message: "Invalid arguments for shareToNotes", | ||
| details: nil)) | ||
| return | ||
| } | ||
|
|
||
| DispatchQueue.main.async { | ||
| let activityViewController = UIActivityViewController(activityItems: [content], applicationActivities: nil) | ||
| activityViewController.completionWithItemsHandler = { _, completed, _, error in | ||
| if let error = error { | ||
| result(FlutterError(code: "SHARE_FAILED", message: error.localizedDescription, details: nil)) | ||
| } else { | ||
| result(completed) | ||
| } | ||
| } | ||
| if let controller = UIApplication.shared.keyWindow?.rootViewController { | ||
| controller.present(activityViewController, animated: true) | ||
| } else { | ||
| result(false) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private func isNotesAppAvailable(result: @escaping FlutterResult) { | ||
| guard let url = URL(string: "mobilenotes://") else { | ||
| result(false) | ||
| return | ||
| } | ||
| result(UIApplication.shared.canOpenURL(url)) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import 'package:omi/backend/preferences.dart'; | ||
|
|
||
| /// An extension on [SharedPreferencesUtil] adding support for storing the | ||
| /// selected export destination for action items. | ||
| /// | ||
| /// This extends the existing preferences API without modifying the upstream | ||
| /// file, which allows our widget to persist the last chosen integration | ||
| /// (e.g. Apple Reminders, Notes or Calendar) across sessions. | ||
| extension TaskExportDestinationExtension on SharedPreferencesUtil { | ||
| /// Persist the user's selected destination for exporting action items. | ||
| /// | ||
| /// The value stored corresponds to the [ActionItemIntegration.name] of | ||
| /// the chosen integration. If an empty string is provided, no value is | ||
| /// stored and the default integration will be used. | ||
| set taskExportDestination(String value) => | ||
| saveString('taskExportDestination', value); | ||
|
|
||
| /// Retrieve the previously selected export destination for action items. | ||
| /// | ||
| /// If no value has been stored, an empty string is returned. Consumers | ||
| /// should interpret the empty string as meaning that the default | ||
| /// integration (Apple Reminders) should be used. | ||
| String get taskExportDestination => | ||
| getString('taskExportDestination') ?? ''; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import 'package:flutter/material.dart'; | ||
|
|
||
| /// Enumeration of possible export destinations for action items. | ||
| /// | ||
| /// Each value defines a human‑readable [displayName] and optionally an | ||
| /// asset name or fallback [icon]. The `assetPath` and `isSvg` fields | ||
| /// describe an optional image resource; however, because the asset files for | ||
| /// Apple Calendar and Apple Notes logos are not included in this patch, we | ||
| /// leave these fields null and rely on the provided [icon] instead. When | ||
| /// [assetPath] is non‑null, [fullAssetPath] prefixes it with | ||
| /// `assets/images/` for use with `Image.asset` or `SvgPicture.asset`. | ||
| enum ActionItemIntegration { | ||
| appleReminders('Apple Reminders', null, false, Icons.notifications), | ||
| appleNotes('Apple Notes', null, false, Icons.note_outlined), | ||
| appleCalendar('Apple Calendar', null, false, Icons.calendar_today); | ||
|
|
||
| /// The display name shown in the UI. | ||
| final String displayName; | ||
|
|
||
| /// The relative path to an image asset within the `assets/images` folder. | ||
| /// If null, no image is used and a fallback [icon] is displayed instead. | ||
| final String? assetPath; | ||
|
|
||
| /// Whether the asset is an SVG. Ignored if [assetPath] is null. | ||
| final bool isSvg; | ||
|
|
||
| /// A fallback Material icon to show when no asset is provided. | ||
| final IconData? icon; | ||
|
|
||
| const ActionItemIntegration( | ||
| this.displayName, | ||
| this.assetPath, | ||
| this.isSvg, | ||
| this.icon, | ||
| ); | ||
|
|
||
| /// Returns the fully qualified asset path, prefixing [assetPath] with | ||
| /// `assets/images/` if an asset is specified. Returns null otherwise. | ||
| String? get fullAssetPath => | ||
| assetPath != null ? 'assets/images/$assetPath' : null; | ||
|
|
||
| /// Indicates whether an image asset is defined for this integration. | ||
| bool get hasAsset => assetPath != null; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Deprecated Key Window Affects Share Sheet
The
shareToNotesmethod usesUIApplication.shared.keyWindowto present the share sheet. This property is deprecated in iOS 13+ and can returnnilon newer iOS versions, which prevents the share sheet from presenting.