diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 5812abf..aea21d2 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -7231,6 +7231,9 @@ } } } + }, + "Calendar App" : { + }, "Calendar or Reminder access is denied. Please enable it in System Settings." : { "comment" : "A message displayed when a user has denied access to the calendar or reminders on their device. It instructs the user to open System Settings to grant access.", @@ -14925,6 +14928,9 @@ } } } + }, + "Enable third-party calendar app launch" : { + }, "Enable third-party DDC app integration" : { "comment" : "A toggle switch that enables or disables integration with a third-party Dynamic Display Control (DDC) app.", @@ -35981,9 +35987,6 @@ } } } - }, - "Use Fantastical" : { - }, "Use MacBook icon when on battery" : { "localizations" : { @@ -37187,7 +37190,7 @@ } } }, - "When enabled, clicking on calendar events will open Fantastical instead of Apple Calendar. More integrations coming soon." : { + "When enabled, clicking on calendar events will open the selected third-party calendar app instead of Apple Calendar." : { }, "When enabled, Focus appears briefly (on/off) and then collapses instead of staying visible." : { diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index ac46e8f..9d39231 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -2752,7 +2752,8 @@ struct CalendarSettings: View { @Default(.lockScreenCalendarSelectionMode) private var lockScreenCalendarSelectionMode @Default(.lockScreenSelectedCalendarIDs) private var lockScreenSelectedCalendarIDs @Default(.lockScreenShowCalendarEventAfterStartEnabled) private var lockScreenShowCalendarEventAfterStartEnabled - @Default(.useFantasticalCalendar) private var useFantasticalCalendar + @Default(.enableThirdPartyCalendarApp) private var enableThirdPartyCalendarApp + @Default(.selectedCalendarApp) private var selectedCalendarApp @Default(.fantasticalDefaultView) private var fantasticalDefaultView private func highlightID(_ title: String) -> String { @@ -3050,28 +3051,37 @@ struct CalendarSettings: View { // MARK: - Third-party Calendar Integration Section { - Defaults.Toggle(key: .useFantasticalCalendar) { + Defaults.Toggle(key: .enableThirdPartyCalendarApp) { HStack { - Image(systemName: "calendar.badge.clock") - .foregroundColor(.red) + Image(systemName: selectedCalendarApp.iconName) + .foregroundColor(selectedCalendarApp.iconColor) .frame(width: 20, height: 20) - Text("Use Fantastical") + Text("Enable third-party calendar app launch") } } - .settingsHighlight(id: highlightID("Use Fantastical")) + .settingsHighlight(id: highlightID("Enable third-party calendar app launch")) - if useFantasticalCalendar { - Picker("Default View", selection: $fantasticalDefaultView) { - ForEach(FantasticalViewStyle.allCases, id: \.self) { style in - Text(style.displayName).tag(style) + if enableThirdPartyCalendarApp { + Picker("Calendar App", selection: $selectedCalendarApp) { + ForEach(ThirdPartyCalendarApp.allCases) { app in + Text(app.displayName).tag(app) } } - .settingsHighlight(id: highlightID("Fantastical Default View")) + .settingsHighlight(id: highlightID("Calendar App")) + + if selectedCalendarApp == .fantastical { + Picker("Default View", selection: $fantasticalDefaultView) { + ForEach(FantasticalViewStyle.allCases, id: \.self) { style in + Text(style.displayName).tag(style) + } + } + .settingsHighlight(id: highlightID("Fantastical Default View")) + } } } header: { Text("Third-party Calendar Integration") } footer: { - Text("When enabled, clicking on calendar events will open Fantastical instead of Apple Calendar. More integrations coming soon.") + Text("When enabled, clicking on calendar events will open the selected third-party calendar app instead of Apple Calendar.") } Section(header: Text("Select Calendars")) { diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 8d28529..a8ad397 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -268,6 +268,34 @@ enum FantasticalViewStyle: String, CaseIterable, Codable, Defaults.Serializable } } +enum ThirdPartyCalendarApp: String, CaseIterable, Codable, Defaults.Serializable, Identifiable { + case fantastical = "fantastical" + case notionCalendar = "notionCalendar" + + var id: String { rawValue } + + var displayName: String { + switch self { + case .fantastical: return "Fantastical" + case .notionCalendar: return "Notion Calendar" + } + } + + var iconName: String { + switch self { + case .fantastical: return "calendar.badge.clock" + case .notionCalendar: return "calendar.badge.plus" + } + } + + var iconColor: Color { + switch self { + case .fantastical: return .red + case .notionCalendar: return .blue + } + } +} + enum ClipboardDisplayMode: String, CaseIterable, Codable, Defaults.Serializable { case popover = "popover" // Traditional popover attached to button case panel = "panel" // Floating panel near notch @@ -867,7 +895,8 @@ extension Defaults.Keys { static let lockScreenWeatherWidgetRowOrder = Key("lockScreenWeatherWidgetRowOrder", default: "weather_calendar_focus") // MARK: Third-party Calendar Integration - static let useFantasticalCalendar = Key("useFantasticalCalendar", default: false) + static let enableThirdPartyCalendarApp = Key("enableThirdPartyCalendarApp", default: false) + static let selectedCalendarApp = Key("selectedCalendarApp", default: .fantastical) static let fantasticalDefaultView = Key("fantasticalDefaultView", default: .mini) // MARK: Battery diff --git a/DynamicIsland/models/EventModel.swift b/DynamicIsland/models/EventModel.swift index 390ef1e..7fbc6cb 100644 --- a/DynamicIsland/models/EventModel.swift +++ b/DynamicIsland/models/EventModel.swift @@ -22,6 +22,7 @@ import Foundation import Defaults +import AppKit struct EventModel: Equatable, Identifiable { let id: String @@ -98,9 +99,14 @@ extension EventModel { var isMeeting: Bool { !participants.isEmpty } func calendarAppURL() -> URL? { - // Check if Fantastical is enabled - if Defaults[.useFantasticalCalendar] { - return fantasticalURL() + // Check if a third-party calendar app is enabled + if Defaults[.enableThirdPartyCalendarApp] { + switch Defaults[.selectedCalendarApp] { + case .fantastical: + return fantasticalURL() + case .notionCalendar: + return notionCalendarURL() + } } return appleCalendarURL() } @@ -160,6 +166,48 @@ extension EventModel { } return URL(string: "x-fantastical3://show/\(viewStyle.rawValue)") } + + /// Returns URL to open event in Notion Calendar (formerly Cron) + private func notionCalendarURL() -> URL? { + // Reminders still use Apple's Reminders app + guard !type.isReminder else { + return URL(string: "x-apple-reminderkit://remcdreminder/\(id)") + } + + let formatter = ISO8601DateFormatter() + + var components = URLComponents() + components.scheme = "cron" + components.host = "showEvent" + components.queryItems = [ + URLQueryItem(name: "accountEmail", value: calendar.accountName), + URLQueryItem(name: "iCalUID", value: id), + URLQueryItem(name: "startDate", value: formatter.string(from: start)), + URLQueryItem(name: "endDate", value: formatter.string(from: end)), + URLQueryItem(name: "title", value: title) + ] + + if let url = components.url { + return url + } + + // Fallback: just launch Notion Calendar app + Self.launchNotionCalendar() + return nil + } + + /// Launches Notion Calendar app directly (fallback or general access) + static func launchNotionCalendar() { + // Launch via bundle identifier (Notion Calendar's bundle ID is "com.cron.electron") + if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.cron.electron") { + let config = NSWorkspace.OpenConfiguration() + NSWorkspace.shared.openApplication(at: url, configuration: config) { _, error in + if let error = error { + print("Error launching Notion Calendar: \(error.localizedDescription)") + } + } + } + } } struct Participant: Hashable {