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 } }