Keine Beschreibung

AppAppearanceManager.swift 2.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. //
  2. // AppAppearanceManager.swift
  3. // App for Indeed
  4. //
  5. import AppKit
  6. /// Persists and applies the user’s light / dark / system appearance preference.
  7. @MainActor
  8. final class AppAppearanceManager {
  9. static let shared = AppAppearanceManager()
  10. static let didChangeNotification = Notification.Name("AppAppearanceManager.didChange")
  11. enum Mode: String, CaseIterable {
  12. case light
  13. case dark
  14. case system
  15. var segmentIndex: Int {
  16. switch self {
  17. case .system: 0
  18. case .light: 1
  19. case .dark: 2
  20. }
  21. }
  22. init?(segmentIndex: Int) {
  23. switch segmentIndex {
  24. case 0: self = .system
  25. case 1: self = .light
  26. case 2: self = .dark
  27. default: return nil
  28. }
  29. }
  30. }
  31. private enum UserDefaultsKey {
  32. static let appearanceMode = "com.appforindeed.appearanceMode"
  33. }
  34. private var systemThemeObserver: NSObjectProtocol?
  35. private init() {
  36. systemThemeObserver = DistributedNotificationCenter.default().addObserver(
  37. forName: Notification.Name("AppleInterfaceThemeChangedNotification"),
  38. object: nil,
  39. queue: .main
  40. ) { [weak self] _ in
  41. guard let self, self.mode == .system else { return }
  42. self.updateWindowChrome()
  43. }
  44. }
  45. var mode: Mode {
  46. get {
  47. guard let raw = UserDefaults.standard.string(forKey: UserDefaultsKey.appearanceMode),
  48. let stored = Mode(rawValue: raw) else {
  49. return .light
  50. }
  51. return stored
  52. }
  53. set {
  54. guard newValue != mode else { return }
  55. UserDefaults.standard.set(newValue.rawValue, forKey: UserDefaultsKey.appearanceMode)
  56. apply()
  57. NotificationCenter.default.post(name: Self.didChangeNotification, object: self)
  58. }
  59. }
  60. /// Window backing color aligned with dashboard chrome for the active appearance.
  61. var windowChromeColor: NSColor {
  62. let isDark = NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
  63. if isDark {
  64. return NSColor(srgbRed: 28 / 255, green: 28 / 255, blue: 30 / 255, alpha: 1)
  65. }
  66. return NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1)
  67. }
  68. func apply() {
  69. switch mode {
  70. case .light:
  71. NSApp.appearance = NSAppearance(named: .aqua)
  72. case .dark:
  73. NSApp.appearance = NSAppearance(named: .darkAqua)
  74. case .system:
  75. NSApp.appearance = nil
  76. }
  77. updateWindowChrome()
  78. }
  79. private func updateWindowChrome() {
  80. let color = windowChromeColor
  81. for window in NSApp.windows where window.isVisible || window.canBecomeKey {
  82. window.backgroundColor = color
  83. }
  84. }
  85. }