From ab7d2619d79bb95805e584188267b02a0e06ace6 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Mon, 6 Apr 2026 11:29:23 +0530 Subject: [PATCH 1/6] Update fantastical open cmds --- DynamicIsland/Localizable.xcstrings | 30 ++++++-------- .../components/Settings/SettingsView.swift | 28 +++++++++++++ DynamicIsland/models/Constants.swift | 16 ++++++++ DynamicIsland/models/EventModel.swift | 39 ++++++++++++++++++- 4 files changed, 94 insertions(+), 19 deletions(-) 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 { From 563afde92f4d9666a3871938c96173ae33e38e3d Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Mon, 6 Apr 2026 13:41:55 +0530 Subject: [PATCH 2/6] Update Notion Calendar --- DynamicIsland/Localizable.xcstrings | 11 ++-- .../components/Settings/SettingsView.swift | 34 +++++++----- DynamicIsland/models/Constants.swift | 31 ++++++++++- DynamicIsland/models/EventModel.swift | 54 +++++++++++++++++-- 4 files changed, 110 insertions(+), 20 deletions(-) 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 { From 78463086bae9d3b62dee0e56d7742f387355c6d3 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Mon, 6 Apr 2026 14:06:34 +0530 Subject: [PATCH 3/6] Update 3rd party calendar icons --- .../components/Settings/SettingsView.swift | 163 ++++++++++++++---- DynamicIsland/models/Constants.swift | 20 ++- 2 files changed, 150 insertions(+), 33 deletions(-) diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 9d39231..3c783db 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -2074,7 +2074,15 @@ private struct ExternalDisplayIntegrationsSection: View { if enableThirdPartyDDCIntegration { Picker("Provider", selection: $thirdPartyDDCProvider) { ForEach(ThirdPartyDDCProvider.allCases) { provider in - Text(provider.displayName).tag(provider) + HStack { + AppIconImage( + bundleIdentifiers: provider.bundleIdentifiers, + symbolFallback: "display", + symbolColor: .secondary + ) + Text(provider.displayName) + } + .tag(provider) } } .settingsHighlight(id: highlightID("Third-party DDC provider")) @@ -3053,9 +3061,12 @@ struct CalendarSettings: View { Section { Defaults.Toggle(key: .enableThirdPartyCalendarApp) { HStack { - Image(systemName: selectedCalendarApp.iconName) - .foregroundColor(selectedCalendarApp.iconColor) - .frame(width: 20, height: 20) + AppIconImage( + bundleIdentifiers: selectedCalendarApp.bundleIdentifiers, + symbolFallback: selectedCalendarApp.fallbackIconName, + symbolColor: selectedCalendarApp.fallbackIconColor, + size: 20 + ) Text("Enable third-party calendar app launch") } } @@ -3064,7 +3075,15 @@ struct CalendarSettings: View { if enableThirdPartyCalendarApp { Picker("Calendar App", selection: $selectedCalendarApp) { ForEach(ThirdPartyCalendarApp.allCases) { app in - Text(app.displayName).tag(app) + HStack { + AppIconImage( + bundleIdentifiers: app.bundleIdentifiers, + symbolFallback: app.fallbackIconName, + symbolColor: app.fallbackIconColor + ) + Text(app.displayName) + } + .tag(app) } } .settingsHighlight(id: highlightID("Calendar App")) @@ -3500,19 +3519,7 @@ struct Shelf: View { Picker("Quick Share Service", selection: $quickShareProvider) { ForEach(quickShareService.availableProviders, id: \.id) { provider in HStack { - Group { - if let imgData = provider.imageData, let nsImg = NSImage(data: imgData) { - Image(nsImage: nsImg) - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - .clipShape(RoundedRectangle(cornerRadius: 3)) - } else { - Image(systemName: "square.and.arrow.up") - .frame(width: 16, height: 16) - } - } - .foregroundColor(.accentColor) + QuickShareProviderIconImage(provider: provider, size: 16) Text(provider.id) } .tag(provider.id) @@ -3523,19 +3530,7 @@ struct Shelf: View { if let selectedProvider { HStack { - Group { - if let imgData = selectedProvider.imageData, let nsImg = NSImage(data: imgData) { - Image(nsImage: nsImg) - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - .clipShape(RoundedRectangle(cornerRadius: 3)) - } else { - Image(systemName: "square.and.arrow.up") - .frame(width: 16, height: 16) - } - } - .foregroundColor(.accentColor) + QuickShareProviderIconImage(provider: selectedProvider, size: 16) VStack(alignment: .leading, spacing: 2) { Text("Currently selected: \(selectedProvider.id)") .font(.caption) @@ -7971,3 +7966,109 @@ struct TerminalSettings: View { .navigationTitle("Terminal") } } + +// MARK: - Reusable App Icon View + +/// Fetches the real app icon from the system using bundle identifiers, +/// falling back to an asset catalog image or an SF Symbol. +struct AppIconImage: View { + let bundleIdentifiers: [String] + var assetFallback: String? = nil + var symbolFallback: String = "app.fill" + var symbolColor: Color = .accentColor + var size: CGFloat = 16 + + var body: some View { + Group { + if let nsImage = resolvedIcon() { + Image(nsImage: nsImage) + .resizable() + .scaledToFit() + .clipShape(RoundedRectangle(cornerRadius: size * 0.2)) + } else if let assetFallback, let nsImage = NSImage(named: NSImage.Name(assetFallback)) { + Image(nsImage: nsImage) + .resizable() + .scaledToFit() + .clipShape(RoundedRectangle(cornerRadius: size * 0.2)) + } else { + Image(systemName: symbolFallback) + .foregroundColor(symbolColor) + } + } + .frame(width: size, height: size) + } + + private func resolvedIcon() -> NSImage? { + for bundleID in bundleIdentifiers { + if let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) { + let icon = NSWorkspace.shared.icon(forFile: appURL.path) + // NSWorkspace returns a valid icon even for generic apps; + // resize to keep memory low. + let thumb = NSImage(size: NSSize(width: 32, height: 32)) + thumb.lockFocus() + icon.draw(in: NSRect(origin: .zero, size: NSSize(width: 32, height: 32)), + from: NSRect(origin: .zero, size: icon.size), + operation: .copy, fraction: 1.0) + thumb.unlockFocus() + return thumb + } + } + return nil + } +} + +private struct QuickShareProviderIconImage: View { + let provider: QuickShareProvider + var size: CGFloat = 16 + + var body: some View { + Group { + if let imgData = provider.imageData, let nsImg = NSImage(data: imgData) { + Image(nsImage: nsImg) + .resizable() + .scaledToFit() + .clipShape(RoundedRectangle(cornerRadius: size * 0.2)) + } else { + AppIconImage( + bundleIdentifiers: provider.bundleIdentifiersFallback, + assetFallback: provider.assetFallbackName, + symbolFallback: provider.symbolFallbackName, + symbolColor: .accentColor, + size: size + ) + } + } + .frame(width: size, height: size) + } +} + +private extension QuickShareProvider { + var bundleIdentifiersFallback: [String] { + switch id { + case "LocalSend": + return ["org.localsend.localsend_app", "org.localsend.localsend"] + case "AirDrop": + return ["com.apple.finder"] + case "Mail": + return ["com.apple.mail"] + case "Messages": + return ["com.apple.MobileSMS", "com.apple.iChat"] + case "Notes": + return ["com.apple.Notes"] + case "Reminders": + return ["com.apple.reminders"] + case "Add to Safari Reading List": + return ["com.apple.Safari"] + default: + return [] + } + } + + var assetFallbackName: String? { + id == "LocalSend" ? "LocalSend" : nil + } + + var symbolFallbackName: String { + id == "System Share Menu" ? "square.and.arrow.up.on.square" : "square.and.arrow.up" + } +} diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index a8ad397..7d9ebc9 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -281,14 +281,22 @@ enum ThirdPartyCalendarApp: String, CaseIterable, Codable, Defaults.Serializable } } - var iconName: String { + /// Bundle identifiers to try when looking up the app icon (first match wins). + var bundleIdentifiers: [String] { + switch self { + case .fantastical: return ["com.flexibits.fantastical2.mac", "com.flexibits.fantastical"] + case .notionCalendar: return ["com.cron.electron"] + } + } + + var fallbackIconName: String { switch self { case .fantastical: return "calendar.badge.clock" case .notionCalendar: return "calendar.badge.plus" } } - var iconColor: Color { + var fallbackIconColor: Color { switch self { case .fantastical: return .red case .notionCalendar: return .blue @@ -370,6 +378,14 @@ enum ThirdPartyDDCProvider: String, CaseIterable, Codable, Defaults.Serializable return "Lunar" } } + + /// Bundle identifiers to try when looking up the app icon. + var bundleIdentifiers: [String] { + switch self { + case .betterDisplay: return ["pro.betterdisplay.BetterDisplay"] + case .lunar: return ["fyi.lunar.Lunar"] + } + } } enum HideNotchOption: String, Defaults.Serializable { From bd2ae3b8bfb6e37ab246f5591b9dae4542cfcf18 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Mon, 6 Apr 2026 14:12:55 +0530 Subject: [PATCH 4/6] Update stuff icons --- DynamicIsland/components/Settings/SettingsView.swift | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 3c783db..95a1051 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -3061,12 +3061,9 @@ struct CalendarSettings: View { Section { Defaults.Toggle(key: .enableThirdPartyCalendarApp) { HStack { - AppIconImage( - bundleIdentifiers: selectedCalendarApp.bundleIdentifiers, - symbolFallback: selectedCalendarApp.fallbackIconName, - symbolColor: selectedCalendarApp.fallbackIconColor, - size: 20 - ) + Image(systemName: "ellipsis.calendar") + .foregroundStyle(.secondary) + .frame(width: 20, height: 20) Text("Enable third-party calendar app launch") } } From 93786b4a9c9a9faabc48ecc6d23755ee6045e08b Mon Sep 17 00:00:00 2001 From: Amir Zarrinkafsh <3339418+nightah@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:23:09 +1000 Subject: [PATCH 5/6] refactor(calendar): open native url schemes from meeting buttons Open conference meetings directly via native app URL schemes instead of routing through the browser. Adds native scheme support for Zoom, Teams, FaceTime, and Discord with fallback to HTTPS. --- .../Calendar/DynamicIslandCalendar.swift | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift b/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift index 11c62dc..c876a1e 100644 --- a/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift +++ b/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift @@ -1010,6 +1010,16 @@ enum ConferenceProvider: CaseIterable { } } + var nativeURLScheme: String? { + switch self { + case .zoom: return "zoommtg" + case .teams: return "msteams" + case .facetime: return "facetime" + case .discord: return "discord" + default: return nil + } + } + static func detect(from url: URL) -> ConferenceProvider { let host = url.host?.lowercased() ?? "" return allCases.first { provider in @@ -1035,7 +1045,17 @@ struct ConferenceJoinButton: View { } var body: some View { - Button(action: { openURL(url) }) { + Button(action: { + if let scheme = provider.nativeURLScheme, + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) { + components.scheme = scheme + if let nativeURL = components.url { + openURL(nativeURL) + return + } + } + openURL(url) + }) { HStack(spacing: 4) { Image(systemName: "video.fill") .font(.system(size: 9)) From 29b80226b40b7a38192c87966cb7d7ec7ab61142 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 8 Apr 2026 13:46:38 +0530 Subject: [PATCH 6/6] Update references to SwiftTerm and rtaudio --- DynamicIsland/audio/AudioBridge.h | 5 +++-- DynamicIsland/audio/AudioBridge.mm | 5 +++-- DynamicIsland/audio/AudioProcessor.hpp | 6 +++--- DynamicIsland/audio/AudioTap.swift | 6 +++--- DynamicIsland/audio/RealTimeAudioSpectrum.swift | 6 +++--- DynamicIsland/managers/TerminalManager.swift | 1 + ReadMe.md | 5 +++++ 7 files changed, 21 insertions(+), 13 deletions(-) diff --git a/DynamicIsland/audio/AudioBridge.h b/DynamicIsland/audio/AudioBridge.h index a2fef37..2f17375 100644 --- a/DynamicIsland/audio/AudioBridge.h +++ b/DynamicIsland/audio/AudioBridge.h @@ -1,11 +1,12 @@ /* * Atoll (DynamicIsland) - * Copyright (C) 2024-2026 Atoll Contributors + * Copyright (C) 2024-2026 Atoll Contributors and zeph + * Adapted from rtaudio (https://github.com/ZephyrCodesStuff/rtaudio) * * Objective-C bridge to expose C++ AudioProcessor to Swift. * * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by + * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. */ diff --git a/DynamicIsland/audio/AudioBridge.mm b/DynamicIsland/audio/AudioBridge.mm index c781c58..e3456f9 100644 --- a/DynamicIsland/audio/AudioBridge.mm +++ b/DynamicIsland/audio/AudioBridge.mm @@ -1,11 +1,12 @@ /* * Atoll (DynamicIsland) - * Copyright (C) 2024-2026 Atoll Contributors + * Copyright (C) 2024-2026 Atoll Contributors and zeph + * Adapted from rtaudio (https://github.com/ZephyrCodesStuff/rtaudio) * * Objective-C++ implementation bridging AudioProcessor to Swift. * * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by + * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. */ diff --git a/DynamicIsland/audio/AudioProcessor.hpp b/DynamicIsland/audio/AudioProcessor.hpp index 970b9f0..72d40f1 100644 --- a/DynamicIsland/audio/AudioProcessor.hpp +++ b/DynamicIsland/audio/AudioProcessor.hpp @@ -1,12 +1,12 @@ /* * Atoll (DynamicIsland) - * Copyright (C) 2024-2026 Atoll Contributors + * Copyright (C) 2024-2026 Atoll Contributors and zeph * * Real-time audio processor using Accelerate framework for efficient FFT - * and band filtering. Adapted from rtaudio project. + * and band filtering. Adapted from rtaudio project (https://github.com/ZephyrCodesStuff/rtaudio) * * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by + * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. */ diff --git a/DynamicIsland/audio/AudioTap.swift b/DynamicIsland/audio/AudioTap.swift index 69ea2d3..60613d4 100644 --- a/DynamicIsland/audio/AudioTap.swift +++ b/DynamicIsland/audio/AudioTap.swift @@ -1,13 +1,13 @@ /* * Atoll (DynamicIsland) - * Copyright (C) 2024-2026 Atoll Contributors + * Copyright (C) 2024-2026 Atoll Contributors and zeph * * CoreAudio tap for capturing real-time audio from music applications. * Uses macOS 14.2+ Process Tap API for efficient audio capture. - * Adapted from rtaudio project. + * Adapted from rtaudio project (https://github.com/ZephyrCodesStuff/rtaudio) * * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by + * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. */ diff --git a/DynamicIsland/audio/RealTimeAudioSpectrum.swift b/DynamicIsland/audio/RealTimeAudioSpectrum.swift index ddb4e8b..fb6688f 100644 --- a/DynamicIsland/audio/RealTimeAudioSpectrum.swift +++ b/DynamicIsland/audio/RealTimeAudioSpectrum.swift @@ -1,12 +1,12 @@ /* * Atoll (DynamicIsland) - * Copyright (C) 2024-2026 Atoll Contributors + * Copyright (C) 2024-2026 Atoll Contributors and zeph * * Real-time audio spectrum visualization using CoreAudio tap data. - * Adapted from rtaudio project. + * Adapted from rtaudio project (https://github.com/ZephyrCodesStuff/rtaudio) * * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by + * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. */ diff --git a/DynamicIsland/managers/TerminalManager.swift b/DynamicIsland/managers/TerminalManager.swift index de0aad7..5b972f1 100644 --- a/DynamicIsland/managers/TerminalManager.swift +++ b/DynamicIsland/managers/TerminalManager.swift @@ -1,6 +1,7 @@ /* * Atoll (DynamicIsland) * Copyright (C) 2024-2026 Atoll Contributors + * Adapted from SwiftTerm (https://github.com/migueldeicaza/SwiftTerm) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/ReadMe.md b/ReadMe.md index 762dba5..9250741 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -112,7 +112,12 @@ Atoll builds upon the work of several open-source projects and draws inspiration - [**SkyLightWindow**](https://github.com/Lakr233/SkyLightWindow) - window rendering for Lock Screen Widgets +- [**rtaudio**](https://github.com/ZephyrCodesStuff/rtaudio) - Live music visualizer using C++ was adapted from this project + +- [**SwiftTerm**](https://github.com/migueldeicaza/SwiftTerm) - Terminal tab implementation in the standard mode was adapted from this project + - Wick - Thanks Nate for allowing us to replicate the iOS like Timer design for the Lock Screen Widget + ## Contributors