Update 3rd party calendar icons

This commit is contained in:
Hariharan Mudaliar 2026-04-06 14:06:34 +05:30
commit 78463086ba
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
2 changed files with 150 additions and 33 deletions

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"))
@ -3053,9 +3061,12 @@ struct CalendarSettings: View {
Section { Section {
Defaults.Toggle(key: .enableThirdPartyCalendarApp) { Defaults.Toggle(key: .enableThirdPartyCalendarApp) {
HStack { HStack {
Image(systemName: selectedCalendarApp.iconName) AppIconImage(
.foregroundColor(selectedCalendarApp.iconColor) bundleIdentifiers: selectedCalendarApp.bundleIdentifiers,
.frame(width: 20, height: 20) symbolFallback: selectedCalendarApp.fallbackIconName,
symbolColor: selectedCalendarApp.fallbackIconColor,
size: 20
)
Text("Enable third-party calendar app launch") Text("Enable third-party calendar app launch")
} }
} }
@ -3064,7 +3075,15 @@ struct CalendarSettings: View {
if enableThirdPartyCalendarApp { if enableThirdPartyCalendarApp {
Picker("Calendar App", selection: $selectedCalendarApp) { Picker("Calendar App", selection: $selectedCalendarApp) {
ForEach(ThirdPartyCalendarApp.allCases) { app in 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")) .settingsHighlight(id: highlightID("Calendar App"))
@ -3500,19 +3519,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)
@ -3523,19 +3530,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)
@ -7971,3 +7966,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

@ -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 { switch self {
case .fantastical: return "calendar.badge.clock" case .fantastical: return "calendar.badge.clock"
case .notionCalendar: return "calendar.badge.plus" case .notionCalendar: return "calendar.badge.plus"
} }
} }
var iconColor: Color { var fallbackIconColor: Color {
switch self { switch self {
case .fantastical: return .red case .fantastical: return .red
case .notionCalendar: return .blue case .notionCalendar: return .blue
@ -370,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 {