BigMarc 197df3298f Customizable shortcuts, app icon, restore-clipboard preference
- Shortcuts screen now records custom global hotkeys (Fix + Ask), persisted and
  re-registered live via Preferences + Combine.
- Add branded AppIcon.icns (gold nib on ink), wired into Info.plist + build.sh.
- "Restore my clipboard after a fix" preference now drives the clipboard fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 14:59:15 +02:00

70 lines
3.1 KiB
Swift

import Foundation
import AppKit
import ServiceManagement
/// User-facing app preferences, persisted to UserDefaults. Kept separate from
/// licensing/entitlements so the General settings stay simple.
@MainActor
final class Preferences: ObservableObject {
enum Appearance: String, CaseIterable, Identifiable {
case system, light, dark
var id: String { rawValue }
var label: String { rawValue.capitalized }
}
enum MenuBarStyle: String, CaseIterable, Identifiable {
case icon, monogram
var id: String { rawValue }
var label: String { self == .icon ? "Icon" : "Monogram" }
}
private let d = UserDefaults.standard
@Published var launchAtLogin: Bool { didSet { applyLaunchAtLogin() } }
@Published var playSounds: Bool { didSet { d.set(playSounds, forKey: "pref.playSounds") } }
@Published var liveUnderlines: Bool { didSet { d.set(liveUnderlines, forKey: "pref.liveUnderlines") } }
@Published var restoreClipboard: Bool { didSet { d.set(restoreClipboard, forKey: "pref.restoreClipboard") } }
@Published var appearance: Appearance { didSet { d.set(appearance.rawValue, forKey: "pref.appearance"); applyAppearance() } }
@Published var menuBarStyle: MenuBarStyle { didSet { d.set(menuBarStyle.rawValue, forKey: "pref.menuBarStyle") } }
@Published var fixHotkey: HotkeyCombo { didSet { persist(fixHotkey, "pref.fixHotkey") } }
@Published var askHotkey: HotkeyCombo { didSet { persist(askHotkey, "pref.askHotkey") } }
init() {
launchAtLogin = SMAppService.mainApp.status == .enabled
playSounds = d.object(forKey: "pref.playSounds") as? Bool ?? true
liveUnderlines = d.object(forKey: "pref.liveUnderlines") as? Bool ?? true
restoreClipboard = d.object(forKey: "pref.restoreClipboard") as? Bool ?? true
appearance = Appearance(rawValue: d.string(forKey: "pref.appearance") ?? "") ?? .system
menuBarStyle = MenuBarStyle(rawValue: d.string(forKey: "pref.menuBarStyle") ?? "") ?? .icon
fixHotkey = Preferences.load("pref.fixHotkey") ?? .defaultFix
askHotkey = Preferences.load("pref.askHotkey") ?? .defaultAsk
}
private func persist(_ combo: HotkeyCombo, _ key: String) {
if let raw = try? JSONEncoder().encode(combo) { d.set(raw, forKey: key) }
}
private static func load(_ key: String) -> HotkeyCombo? {
guard let raw = UserDefaults.standard.data(forKey: key) else { return nil }
return try? JSONDecoder().decode(HotkeyCombo.self, from: raw)
}
private func applyLaunchAtLogin() {
do {
if launchAtLogin { try SMAppService.mainApp.register() }
else { try SMAppService.mainApp.unregister() }
} catch {
Log.app.error("Launch-at-login change failed: \(error.localizedDescription, privacy: .public)")
}
}
private func applyAppearance() {
switch appearance {
case .system: NSApp.appearance = nil
case .light: NSApp.appearance = NSAppearance(named: .aqua)
case .dark: NSApp.appearance = NSAppearance(named: .darkAqua)
}
}
}