| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- import AppKit
- import SwiftUI
- /// Manages the app icon color, allowing users to customize the Dock icon tint.
- /// Persists selection to UserDefaults and applies it via NSApplication.shared.applicationIconImage.
- final class AppIconConfig: ObservableObject {
- static let shared = AppIconConfig()
- /// Available icon color options, matching the iOS app.
- struct IconColorOption: Identifiable {
- let id: String // name
- let name: String
- let color: Color
- let nsColor: NSColor
- init(_ name: String, _ color: Color, _ nsColor: NSColor) {
- self.id = name
- self.name = name
- self.color = color
- self.nsColor = nsColor
- }
- }
- static let iconColors: [IconColorOption] = [
- IconColorOption("Default", .green, NSColor(red: 0.35, green: 0.85, blue: 0.25, alpha: 1)),
- IconColorOption("Green", Color(red: 0.35, green: 0.85, blue: 0.25), NSColor(red: 0.35, green: 0.85, blue: 0.25, alpha: 1)),
- IconColorOption("Lime", Color(red: 0.55, green: 0.95, blue: 0.15), NSColor(red: 0.55, green: 0.95, blue: 0.15, alpha: 1)),
- IconColorOption("Cyan", Color(red: 0.15, green: 0.85, blue: 0.85), NSColor(red: 0.15, green: 0.85, blue: 0.85, alpha: 1)),
- IconColorOption("Blue", Color(red: 0.25, green: 0.45, blue: 0.95), NSColor(red: 0.25, green: 0.45, blue: 0.95, alpha: 1)),
- IconColorOption("Purple", Color(red: 0.6, green: 0.3, blue: 0.9), NSColor(red: 0.6, green: 0.3, blue: 0.9, alpha: 1)),
- IconColorOption("Pink", Color(red: 0.95, green: 0.3, blue: 0.6), NSColor(red: 0.95, green: 0.3, blue: 0.6, alpha: 1)),
- IconColorOption("Red", Color(red: 0.95, green: 0.25, blue: 0.25), NSColor(red: 0.95, green: 0.25, blue: 0.25, alpha: 1)),
- IconColorOption("Orange", Color(red: 0.95, green: 0.55, blue: 0.15), NSColor(red: 0.95, green: 0.55, blue: 0.15, alpha: 1)),
- IconColorOption("Gold", Color(red: 0.95, green: 0.8, blue: 0.15), NSColor(red: 0.95, green: 0.8, blue: 0.15, alpha: 1)),
- IconColorOption("White", Color(red: 0.9, green: 0.9, blue: 0.92), NSColor(red: 0.9, green: 0.9, blue: 0.92, alpha: 1)),
- ]
- @Published var selectedColorName: String {
- didSet {
- UserDefaults.standard.set(selectedColorName, forKey: "appIconColorName")
- applyIcon()
- }
- }
- private init() {
- self.selectedColorName = UserDefaults.standard.string(forKey: "appIconColorName") ?? "Default"
- }
- /// The currently selected color option.
- var selectedOption: IconColorOption {
- Self.iconColors.first(where: { $0.name == selectedColorName }) ?? Self.iconColors[0]
- }
- /// Apply the selected icon color to the Dock icon.
- func applyIcon() {
- if selectedColorName == "Default" {
- // Reset to the bundle icon
- NSApplication.shared.applicationIconImage = nil
- return
- }
- guard let option = Self.iconColors.first(where: { $0.name == selectedColorName }) else { return }
- guard let tintedIcon = generateTintedIcon(color: option.nsColor) else { return }
- NSApplication.shared.applicationIconImage = tintedIcon
- }
- /// Generate the app icon tinted with the given color.
- /// Loads the default icon from the bundle, desaturates it, and applies the new color.
- private func generateTintedIcon(color: NSColor) -> NSImage? {
- // Load the high-res icon from the asset catalog
- guard let bundleIcon = loadBundleIcon() else { return nil }
- guard let cgImage = bundleIcon.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil }
- let width = cgImage.width
- let height = cgImage.height
- guard let context = CGContext(
- data: nil,
- width: width,
- height: height,
- bitsPerComponent: 8,
- bytesPerRow: 0,
- space: CGColorSpaceCreateDeviceRGB(),
- bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
- ) else { return nil }
- let rect = CGRect(x: 0, y: 0, width: width, height: height)
- // 1) Draw the original icon (to get its luminance structure)
- context.draw(cgImage, in: rect)
- // 2) Apply color overlay with .color blend mode (preserves luminance, applies hue+saturation)
- context.setBlendMode(.color)
- context.setFillColor(color.cgColor)
- context.fill(rect)
- // 3) Clip to original alpha by drawing original with .destinationIn
- context.setBlendMode(.destinationIn)
- context.draw(cgImage, in: rect)
- guard let resultCG = context.makeImage() else { return nil }
- let result = NSImage(cgImage: resultCG, size: NSSize(width: width, height: height))
- return result
- }
- /// Load the 1024px icon from the app bundle's asset catalog.
- private func loadBundleIcon() -> NSImage? {
- // Try loading from bundle resources directly
- if let url = Bundle.main.url(forResource: "icon_1024", withExtension: "png"),
- let img = NSImage(contentsOf: url) {
- return img
- }
- // Fallback: use the app icon from the bundle
- if let iconName = Bundle.main.infoDictionary?["CFBundleIconFile"] as? String,
- let img = NSImage(named: iconName) {
- return img
- }
- // Final fallback: NSApp icon
- return NSApp.applicationIconImage
- }
- }
|