Merge remote-tracking branch 'origin/main' into feature/chinese-localization

This commit is contained in:
Hariharan Mudaliar 2026-04-08 13:56:45 +05:30
commit c09be0b12b
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
12 changed files with 5469 additions and 5172 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,12 @@
/* /*
* Atoll (DynamicIsland) * 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. * Objective-C bridge to expose C++ AudioProcessor to Swift.
* *
* This program is free software: you can redistribute it and/or modify * 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 * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
*/ */

View file

@ -1,11 +1,12 @@
/* /*
* Atoll (DynamicIsland) * 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. * Objective-C++ implementation bridging AudioProcessor to Swift.
* *
* This program is free software: you can redistribute it and/or modify * 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 * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
*/ */

View file

@ -1,12 +1,12 @@
/* /*
* Atoll (DynamicIsland) * 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 * 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 * 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 * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
*/ */

View file

@ -1,13 +1,13 @@
/* /*
* Atoll (DynamicIsland) * 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. * CoreAudio tap for capturing real-time audio from music applications.
* Uses macOS 14.2+ Process Tap API for efficient audio capture. * 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 * 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 * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
*/ */

View file

@ -1,12 +1,12 @@
/* /*
* Atoll (DynamicIsland) * 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. * 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 * 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 * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
*/ */

View file

@ -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 { static func detect(from url: URL) -> ConferenceProvider {
let host = url.host?.lowercased() ?? "" let host = url.host?.lowercased() ?? ""
return allCases.first { provider in return allCases.first { provider in
@ -1035,7 +1045,17 @@ struct ConferenceJoinButton: View {
} }
var body: some 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) { HStack(spacing: 4) {
Image(systemName: "video.fill") Image(systemName: "video.fill")
.font(.system(size: 9)) .font(.system(size: 9))

View file

@ -2074,7 +2074,15 @@ private struct ExternalDisplayIntegrationsSection: View {
if enableThirdPartyDDCIntegration { if enableThirdPartyDDCIntegration {
Picker("Provider", selection: $thirdPartyDDCProvider) { Picker("Provider", selection: $thirdPartyDDCProvider) {
ForEach(ThirdPartyDDCProvider.allCases) { provider in 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")) .settingsHighlight(id: highlightID("Third-party DDC provider"))
@ -2752,6 +2760,9 @@ struct CalendarSettings: View {
@Default(.lockScreenCalendarSelectionMode) private var lockScreenCalendarSelectionMode @Default(.lockScreenCalendarSelectionMode) private var lockScreenCalendarSelectionMode
@Default(.lockScreenSelectedCalendarIDs) private var lockScreenSelectedCalendarIDs @Default(.lockScreenSelectedCalendarIDs) private var lockScreenSelectedCalendarIDs
@Default(.lockScreenShowCalendarEventAfterStartEnabled) private var lockScreenShowCalendarEventAfterStartEnabled @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 { private func highlightID(_ title: String) -> String {
SettingsTab.calendar.highlightID(for: title) SettingsTab.calendar.highlightID(for: title)
@ -3046,6 +3057,49 @@ struct CalendarSettings: View {
.settingsHighlight(id: highlightID("Show start time after event begins")) .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")) { Section(header: Text("Select Calendars")) {
let grouped = Dictionary(grouping: calendarManager.allCalendars, by: \.accountName) let grouped = Dictionary(grouping: calendarManager.allCalendars, by: \.accountName)
let sortedAccounts = grouped.keys.sorted() let sortedAccounts = grouped.keys.sorted()
@ -3462,19 +3516,7 @@ struct Shelf: View {
Picker("Quick Share Service", selection: $quickShareProvider) { Picker("Quick Share Service", selection: $quickShareProvider) {
ForEach(quickShareService.availableProviders, id: \.id) { provider in ForEach(quickShareService.availableProviders, id: \.id) { provider in
HStack { HStack {
Group { QuickShareProviderIconImage(provider: provider, size: 16)
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)
Text(provider.id) Text(provider.id)
} }
.tag(provider.id) .tag(provider.id)
@ -3485,19 +3527,7 @@ struct Shelf: View {
if let selectedProvider { if let selectedProvider {
HStack { HStack {
Group { QuickShareProviderIconImage(provider: selectedProvider, size: 16)
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)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("Currently selected: \(selectedProvider.id)") Text("Currently selected: \(selectedProvider.id)")
.font(.caption) .font(.caption)
@ -7933,3 +7963,109 @@ struct TerminalSettings: View {
.navigationTitle("Terminal") .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"
}
}

View file

@ -1,6 +1,7 @@
/* /*
* Atoll (DynamicIsland) * Atoll (DynamicIsland)
* Copyright (C) 2024-2026 Atoll Contributors * 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 * 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 General Public License as published by

View file

@ -256,6 +256,54 @@ enum CalendarSelectionState: Codable, Defaults.Serializable {
case selected(Set<String>) case selected(Set<String>)
} }
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 { enum ClipboardDisplayMode: String, CaseIterable, Codable, Defaults.Serializable {
case popover = "popover" // Traditional popover attached to button case popover = "popover" // Traditional popover attached to button
case panel = "panel" // Floating panel near notch case panel = "panel" // Floating panel near notch
@ -330,6 +378,14 @@ enum ThirdPartyDDCProvider: String, CaseIterable, Codable, Defaults.Serializable
return "Lunar" 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 { enum HideNotchOption: String, Defaults.Serializable {
@ -854,6 +910,11 @@ extension Defaults.Keys {
static let lockScreenShowCalendarStartTimeAfterBegins = Key<Bool>("lockScreenShowCalendarStartTimeAfterBegins", default: true) static let lockScreenShowCalendarStartTimeAfterBegins = Key<Bool>("lockScreenShowCalendarStartTimeAfterBegins", default: true)
static let lockScreenWeatherWidgetRowOrder = Key<String>("lockScreenWeatherWidgetRowOrder", default: "weather_calendar_focus") static let lockScreenWeatherWidgetRowOrder = Key<String>("lockScreenWeatherWidgetRowOrder", default: "weather_calendar_focus")
// MARK: Third-party Calendar Integration
static let enableThirdPartyCalendarApp = Key<Bool>("enableThirdPartyCalendarApp", default: false)
static let selectedCalendarApp = Key<ThirdPartyCalendarApp>("selectedCalendarApp", default: .fantastical)
static let fantasticalDefaultView = Key<FantasticalViewStyle>("fantasticalDefaultView", default: .mini)
// MARK: Battery // MARK: Battery
static let showPowerStatusNotifications = Key<Bool>("showPowerStatusNotifications", default: true) static let showPowerStatusNotifications = Key<Bool>("showPowerStatusNotifications", default: true)
static let showBatteryIndicator = Key<Bool>("showBatteryIndicator", default: BatteryActivityManager.shared.hasBattery()) static let showBatteryIndicator = Key<Bool>("showBatteryIndicator", default: BatteryActivityManager.shared.hasBattery())

View file

@ -21,6 +21,8 @@
*/ */
import Foundation import Foundation
import Defaults
import AppKit
struct EventModel: Equatable, Identifiable { struct EventModel: Equatable, Identifiable {
let id: String let id: String
@ -97,7 +99,20 @@ extension EventModel {
var isMeeting: Bool { !participants.isEmpty } var isMeeting: Bool { !participants.isEmpty }
func calendarAppURL() -> URL? { 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 { guard let id = id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
return nil return nil
} }
@ -123,6 +138,76 @@ extension EventModel {
} }
return URL(string: "ical://ekevent\(date)/\(id)?method=show&options=more") 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 { struct Participant: Hashable {

View file

@ -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 - [**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 - Wick - Thanks Nate for allowing us to replicate the iOS like Timer design for the Lock Screen Widget
## Contributors ## Contributors
<a href="https://github.com/Ebullioscopic/Atoll/graphs/contributors"> <a href="https://github.com/Ebullioscopic/Atoll/graphs/contributors">