diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 31e415e..5812abf 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -2042,7 +2042,6 @@ }, "±10 Seconds" : { "comment" : "Display name for the \"±10 Seconds\" music skip behavior option.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -2665,7 +2664,6 @@ }, "Accent" : { "comment" : "Display name for the caps lock indicator tint mode.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -3143,7 +3141,6 @@ }, "Adaptive gradient" : { "comment" : "Display name for the timer icon color mode: adaptive gradient.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -5194,7 +5191,6 @@ }, "Apple Music" : { "comment" : "Media controller type for Apple Music", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -5917,7 +5913,6 @@ }, "Bar" : { "comment" : "Name of the timer progress style that shows a horizontal bar.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -11787,6 +11782,9 @@ } } } + }, + "Default View" : { + }, "Default: dynamic.m4a" : { "localizations" : { @@ -12212,7 +12210,6 @@ }, "Digital" : { "comment" : "The display name for the \"Digital\" reminder presentation style.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -17300,7 +17297,6 @@ }, "Green" : { "comment" : "Display name for the green caps lock indicator tint mode.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -25124,7 +25120,6 @@ }, "Now Playing" : { "comment" : "Name of the media controller type for the \"Now Playing\" option.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -25809,7 +25804,6 @@ }, "Panel" : { "comment" : "Display name for the clipboard display mode \"Panel\".", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -28661,7 +28655,6 @@ }, "Ring" : { "comment" : "The name of the timer progress style that shows a ring shape.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -29919,7 +29912,6 @@ }, "Separate Tab" : { "comment" : "Display name for the clipboard display mode option to open the clipboard in a separate tab.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -31687,7 +31679,6 @@ }, "Shows screen assistant as a dropdown attached to the AI button" : { "comment" : "Description of the screen assistant display mode option that shows the screen assistant as a dropdown attached to the AI button.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -31700,7 +31691,6 @@ }, "Shows screen assistant in a floating panel near the notch" : { "comment" : "Description of the screen assistant display mode option that shows the screen assistant in a floating panel near the notch.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -31854,7 +31844,6 @@ }, "Skip forward or backward by ten seconds" : { "comment" : "Description of a music skip behavior option.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -32517,7 +32506,6 @@ }, "Spotify" : { "comment" : "Spotify media controller type.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -32706,7 +32694,6 @@ }, "Standard previous/next track controls" : { "comment" : "Description of the music skip behavior option to use standard previous/next track controls.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -34131,6 +34118,9 @@ } } } + }, + "Third-party Calendar Integration" : { + }, "This duration powers the \"Custom\" option inside the timer popover for quick access." : { "comment" : "A footer description for the custom timer settings, explaining their purpose.", @@ -34995,7 +34985,6 @@ }, "Track Skip" : { "comment" : "Display name for the \"Track Skip\" music skip behavior.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -35992,6 +35981,9 @@ } } } + }, + "Use Fantastical" : { + }, "Use MacBook icon when on battery" : { "localizations" : { @@ -37194,6 +37186,9 @@ } } } + }, + "When enabled, clicking on calendar events will open Fantastical instead of Apple Calendar. More integrations coming soon." : { + }, "When enabled, Focus appears briefly (on/off) and then collapses instead of staying visible." : { "comment" : "A help text for the setting that allows the focus indicator to collapse instead of staying visible.", @@ -37763,7 +37758,6 @@ }, "Youtube Music" : { "comment" : "Spotify music service.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 58dfa77..ac46e8f 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -2752,6 +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(.fantasticalDefaultView) private var fantasticalDefaultView private func highlightID(_ title: String) -> String { SettingsTab.calendar.highlightID(for: title) @@ -3045,6 +3047,32 @@ struct CalendarSettings: View { .disabled(!lockScreenShowCalendarEvent) .settingsHighlight(id: highlightID("Show start time after event begins")) } + + // MARK: - Third-party Calendar Integration + Section { + Defaults.Toggle(key: .useFantasticalCalendar) { + HStack { + Image(systemName: "calendar.badge.clock") + .foregroundColor(.red) + .frame(width: 20, height: 20) + Text("Use Fantastical") + } + } + .settingsHighlight(id: highlightID("Use Fantastical")) + + if useFantasticalCalendar { + 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.") + } Section(header: Text("Select Calendars")) { let grouped = Dictionary(grouping: calendarManager.allCalendars, by: \.accountName) diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 77e53b8..8d28529 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -256,6 +256,18 @@ enum CalendarSelectionState: Codable, Defaults.Serializable { case selected(Set) } +enum FantasticalViewStyle: String, CaseIterable, Codable, Defaults.Serializable { + case mini = "mini" + case calendar = "calendar" + + var displayName: String { + switch self { + case .mini: return "Mini View" + case .calendar: return "Full Calendar" + } + } +} + enum ClipboardDisplayMode: String, CaseIterable, Codable, Defaults.Serializable { case popover = "popover" // Traditional popover attached to button case panel = "panel" // Floating panel near notch @@ -854,6 +866,10 @@ extension Defaults.Keys { static let lockScreenShowCalendarStartTimeAfterBegins = Key("lockScreenShowCalendarStartTimeAfterBegins", default: true) static let lockScreenWeatherWidgetRowOrder = Key("lockScreenWeatherWidgetRowOrder", default: "weather_calendar_focus") + // MARK: Third-party Calendar Integration + static let useFantasticalCalendar = Key("useFantasticalCalendar", default: false) + static let fantasticalDefaultView = Key("fantasticalDefaultView", default: .mini) + // MARK: Battery static let showPowerStatusNotifications = Key("showPowerStatusNotifications", default: true) static let showBatteryIndicator = Key("showBatteryIndicator", default: BatteryActivityManager.shared.hasBattery()) diff --git a/DynamicIsland/models/EventModel.swift b/DynamicIsland/models/EventModel.swift index fe42bd2..390ef1e 100644 --- a/DynamicIsland/models/EventModel.swift +++ b/DynamicIsland/models/EventModel.swift @@ -21,6 +21,7 @@ */ import Foundation +import Defaults struct EventModel: Equatable, Identifiable { let id: String @@ -97,7 +98,15 @@ extension EventModel { var isMeeting: Bool { !participants.isEmpty } func calendarAppURL() -> URL? { - + // Check if Fantastical is enabled + if Defaults[.useFantasticalCalendar] { + return fantasticalURL() + } + return appleCalendarURL() + } + + /// Returns URL to open event in Apple Calendar + private func appleCalendarURL() -> URL? { guard let id = id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { return nil } @@ -123,6 +132,34 @@ extension EventModel { } return URL(string: "ical://ekevent\(date)/\(id)?method=show&options=more") } + + /// Returns URL to open date in Fantastical + private func fantasticalURL() -> URL? { + // Reminders still use Apple's Reminders app + guard !type.isReminder else { + return URL(string: "x-apple-reminderkit://remcdreminder/\(id)") + } + + let viewStyle = Defaults[.fantasticalDefaultView] + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let dateString = dateFormatter.string(from: start) + + // x-fantastical3://show/mini/yyyy-MM-dd or x-fantastical3://show/calendar/yyyy-MM-dd + return URL(string: "x-fantastical3://show/\(viewStyle.rawValue)/\(dateString)") + } + + /// Returns URL to open Fantastical at current date (for general calendar access) + static func fantasticalShowURL(for date: Date? = nil) -> URL? { + let viewStyle = Defaults[.fantasticalDefaultView] + if let date = date { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let dateString = dateFormatter.string(from: date) + return URL(string: "x-fantastical3://show/\(viewStyle.rawValue)/\(dateString)") + } + return URL(string: "x-fantastical3://show/\(viewStyle.rawValue)") + } } struct Participant: Hashable {