import SwiftUI import AppKit /// Die einstellbaren Farben. /// /// **Nicht alle Farben sind einstellbar, und das ist Absicht.** Gelb heißt /// Warnung, Rot heißt kritisch, Grün heißt in Ordnung — bei Temperatur, Akku, /// Lüfter und Fehlermeldungen. Wer sie umstellen kann, kann ihre Bedeutung /// zerstören; ein rotes „alles gut" liest niemand richtig. Einstellbar ist /// deshalb die Akzentfarbe, mit der Onyx sich selbst zeichnet, und das Paar für /// Herunter- und Hochladen — dort ist die Zuordnung willkürlich und nur eine /// Gewohnheit. @MainActor @Observable public final class OnyxTheme { /// Die Instanz, aus der `Onyx.Color` liest. public static let shared = OnyxTheme() /// Kühl und entsättigt — Farbe ist in Onyx ein Signal, keine Dekoration. public static let defaultAccent = Color(red: 0.498, green: 0.659, blue: 1.0) /// Herunterladen grün, Hochladen rosa: die Zuordnung ist reine Gewohnheit, /// aber eine verbreitete. Wer sie anders kennt, dreht sie um. public static let defaultDownload = Color(red: 0.435, green: 0.812, blue: 0.592) public static let defaultUpload = Color(red: 0.937, green: 0.427, blue: 0.612) public var accent: Color { didSet { store(accent, as: Keys.accent) } } public var download: Color { didSet { store(download, as: Keys.download) } } public var upload: Color { didSet { store(upload, as: Keys.upload) } } private let defaults: UserDefaults private enum Keys { static let accent = "onyx.theme.accent" static let download = "onyx.theme.download" static let upload = "onyx.theme.upload" } public init(defaults: UserDefaults = .standard) { self.defaults = defaults accent = Color(onyxHex: defaults.string(forKey: Keys.accent)) ?? Self.defaultAccent download = Color(onyxHex: defaults.string(forKey: Keys.download)) ?? Self.defaultDownload upload = Color(onyxHex: defaults.string(forKey: Keys.upload)) ?? Self.defaultUpload } /// Ob alles auf den Werkseinstellungen steht — für den Zurücksetzen-Knopf, /// der sonst auch dann anböte, etwas zu tun, wenn es nichts zu tun gibt. public var isDefault: Bool { accent.onyxHex == Self.defaultAccent.onyxHex && download.onyxHex == Self.defaultDownload.onyxHex && upload.onyxHex == Self.defaultUpload.onyxHex } public func reset() { accent = Self.defaultAccent download = Self.defaultDownload upload = Self.defaultUpload } /// Übernimmt die Akzentfarbe des Systems. /// /// Genau die stand hier vorher an vielen Stellen — unfreiwillig, weil /// `.tint` und `.accentColor` ohne eigene Angabe dorthin durchgereicht /// werden. Wem das gefiel, soll es behalten können. public func adoptSystemAccent() { accent = Color(nsColor: .controlAccentColor) } private func store(_ color: Color, as key: String) { defaults.set(color.onyxHex, forKey: key) } } public extension Color { /// `#RRGGBB`, immer in sRGB. /// /// Der Umweg über `NSColor` ist nötig, weil eine `Color` aus dem Farbwähler /// in einem beliebigen Farbraum liegen kann — ohne Umrechnung ergäbe die /// Zerlegung in Komponenten je nach Bildschirm etwas anderes. var onyxHex: String { guard let srgb = NSColor(self).usingColorSpace(.sRGB) else { return "#7FA8FF" } let red = Int((srgb.redComponent * 255).rounded()) let green = Int((srgb.greenComponent * 255).rounded()) let blue = Int((srgb.blueComponent * 255).rounded()) return String(format: "#%02X%02X%02X", red, green, blue) } /// Liest `#RRGGBB` und `RRGGBB`. init?(onyxHex: String?) { guard let onyxHex else { return nil } var text = onyxHex.trimmingCharacters(in: .whitespaces) if text.hasPrefix("#") { text.removeFirst() } guard text.count == 6, let value = UInt32(text, radix: 16) else { return nil } self.init(.sRGB, red: Double((value >> 16) & 0xFF) / 255, green: Double((value >> 8) & 0xFF) / 255, blue: Double(value & 0xFF) / 255) } }