diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 31e415e..aea21d2 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" : { @@ -7236,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.", @@ -11787,6 +11785,9 @@ } } } + }, + "Default View" : { + }, "Default: dynamic.m4a" : { "localizations" : { @@ -12212,7 +12213,6 @@ }, "Digital" : { "comment" : "The display name for the \"Digital\" reminder presentation style.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -14928,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.", @@ -17300,7 +17303,6 @@ }, "Green" : { "comment" : "Display name for the green caps lock indicator tint mode.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -25124,7 +25126,6 @@ }, "Now Playing" : { "comment" : "Name of the media controller type for the \"Now Playing\" option.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -25809,7 +25810,6 @@ }, "Panel" : { "comment" : "Display name for the clipboard display mode \"Panel\".", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -28661,7 +28661,6 @@ }, "Ring" : { "comment" : "The name of the timer progress style that shows a ring shape.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -29919,7 +29918,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 +31685,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 +31697,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 +31850,6 @@ }, "Skip forward or backward by ten seconds" : { "comment" : "Description of a music skip behavior option.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -32517,7 +32512,6 @@ }, "Spotify" : { "comment" : "Spotify media controller type.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -32706,7 +32700,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 +34124,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 +34991,6 @@ }, "Track Skip" : { "comment" : "Display name for the \"Track Skip\" music skip behavior.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -37194,6 +37189,9 @@ } } } + }, + "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." : { "comment" : "A help text for the setting that allows the focus indicator to collapse instead of staying visible.", @@ -37763,7 +37761,6 @@ }, "Youtube Music" : { "comment" : "Spotify music service.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { 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/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 58dfa77..95a1051 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")) @@ -2752,6 +2760,9 @@ struct CalendarSettings: View { @Default(.lockScreenCalendarSelectionMode) private var lockScreenCalendarSelectionMode @Default(.lockScreenSelectedCalendarIDs) private var lockScreenSelectedCalendarIDs @Default(.lockScreenShowCalendarEventAfterStartEnabled) private var lockScreenShowCalendarEventAfterStartEnabled + @Default(.enableThirdPartyCalendarApp) private var enableThirdPartyCalendarApp + @Default(.selectedCalendarApp) private var selectedCalendarApp + @Default(.fantasticalDefaultView) private var fantasticalDefaultView private func highlightID(_ title: String) -> String { SettingsTab.calendar.highlightID(for: title) @@ -3045,6 +3056,49 @@ struct CalendarSettings: View { .disabled(!lockScreenShowCalendarEvent) .settingsHighlight(id: highlightID("Show start time after event begins")) } + + // MARK: - Third-party Calendar Integration + Section { + Defaults.Toggle(key: .enableThirdPartyCalendarApp) { + HStack { + Image(systemName: "ellipsis.calendar") + .foregroundStyle(.secondary) + .frame(width: 20, height: 20) + Text("Enable third-party calendar app launch") + } + } + .settingsHighlight(id: highlightID("Enable third-party calendar app launch")) + + if enableThirdPartyCalendarApp { + Picker("Calendar App", selection: $selectedCalendarApp) { + ForEach(ThirdPartyCalendarApp.allCases) { app in + HStack { + AppIconImage( + bundleIdentifiers: app.bundleIdentifiers, + symbolFallback: app.fallbackIconName, + symbolColor: app.fallbackIconColor + ) + Text(app.displayName) + } + .tag(app) + } + } + .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 the selected third-party calendar app instead of Apple Calendar.") + } Section(header: Text("Select Calendars")) { let grouped = Dictionary(grouping: calendarManager.allCalendars, by: \.accountName) @@ -3462,19 +3516,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) @@ -3485,19 +3527,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) @@ -7933,3 +7963,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/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/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 77e53b8..7d9ebc9 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -256,6 +256,54 @@ 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 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" + } + } + + /// 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 fallbackIconColor: 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 @@ -330,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 { @@ -854,6 +910,11 @@ 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 enableThirdPartyCalendarApp = Key("enableThirdPartyCalendarApp", default: false) + static let selectedCalendarApp = Key("selectedCalendarApp", default: .fantastical) + 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..7fbc6cb 100644 --- a/DynamicIsland/models/EventModel.swift +++ b/DynamicIsland/models/EventModel.swift @@ -21,6 +21,8 @@ */ import Foundation +import Defaults +import AppKit struct EventModel: Equatable, Identifiable { let id: String @@ -97,7 +99,20 @@ extension EventModel { var isMeeting: Bool { !participants.isEmpty } func calendarAppURL() -> URL? { - + // 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() + } + + /// Returns URL to open event in Apple Calendar + private func appleCalendarURL() -> URL? { guard let id = id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { return nil } @@ -123,6 +138,76 @@ 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)") + } + + /// 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 { 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