Преглед на файлове

Add System, Light, and Dark theme picker in Settings.

Persist appearance preference and apply it at launch so users can match macOS or force light or dark chrome.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 преди 2 седмици
родител
ревизия
ce75d79d9c
променени са 3 файла, в които са добавени 142 реда и са изтрити 4 реда
  1. 3 4
      App for Indeed/AppDelegate.swift
  2. 98 0
      App for Indeed/Services/AppAppearanceManager.swift
  3. 41 0
      App for Indeed/Views/DashboardView.swift

+ 3 - 4
App for Indeed/AppDelegate.swift

@@ -20,8 +20,8 @@ enum AppWindowConfiguration {
20 20
         window.titlebarAppearsTransparent = true
21 21
         window.titleVisibility = .hidden
22 22
         window.isMovableByWindowBackground = true
23
-        // Same as `DashboardView` chrome — avoids a white halo outside the grey frame with fullSizeContentView.
24
-        window.backgroundColor = NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1)
23
+        // Same as dashboard chrome — avoids a halo outside the frame with fullSizeContentView.
24
+        window.backgroundColor = AppAppearanceManager.shared.windowChromeColor
25 25
 
26 26
         let targetContent = NSRect(origin: .zero, size: defaultContentSize)
27 27
         let targetFrame = window.frameRect(forContentRect: targetContent)
@@ -48,8 +48,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
48 48
     private var lastSubscriptionRefreshAt: Date?
49 49
 
50 50
     func applicationWillFinishLaunching(_ notification: Notification) {
51
-        // Dashboard is light-themed; without this, a Dark Mode Mac paints a dark title bar.
52
-        NSApp.appearance = NSAppearance(named: .aqua)
51
+        AppAppearanceManager.shared.apply()
53 52
     }
54 53
 
55 54
     func applicationDidFinishLaunching(_ aNotification: Notification) {

+ 98 - 0
App for Indeed/Services/AppAppearanceManager.swift

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

+ 41 - 0
App for Indeed/Views/DashboardView.swift

@@ -119,6 +119,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
119 119
     private let savedJobsDocumentView = JobListingsDocumentView()
120 120
     private let savedJobsStack = NSStackView()
121 121
     private let settingsPageContainer = NSView()
122
+    private weak var appearanceModeSegment: NSSegmentedControl?
122 123
     private let cvMakerPageContainer = NSView()
123 124
     private lazy var cvMakerPageView: CVMakerPageView = {
124 125
         CVMakerPageView()
@@ -1557,6 +1558,23 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1557 1558
         contentStack.alignment = .leading
1558 1559
         contentStack.translatesAutoresizingMaskIntoConstraints = false
1559 1560
 
1561
+        let appearanceTitle = NSTextField(labelWithString: "Appearance")
1562
+        appearanceTitle.font = .systemFont(ofSize: 12, weight: .semibold)
1563
+        appearanceTitle.textColor = Theme.secondaryText
1564
+        appearanceTitle.alignment = .left
1565
+
1566
+        let themeSegment = makeAppearanceModeSegment()
1567
+        appearanceModeSegment = themeSegment
1568
+        let appearanceSection = makeSettingsSection(rows: [
1569
+            makeSettingsRow(title: "Theme", systemImage: "circle.lefthalf.filled", accessory: themeSegment, tapAction: nil)
1570
+        ])
1571
+
1572
+        let appearanceStack = NSStackView(views: [appearanceTitle, appearanceSection])
1573
+        appearanceStack.orientation = .vertical
1574
+        appearanceStack.spacing = 14
1575
+        appearanceStack.alignment = .leading
1576
+        appearanceStack.translatesAutoresizingMaskIntoConstraints = false
1577
+
1560 1578
         let settingsSection = makeSettingsSection(rows: [
1561 1579
             makeSettingsRow(title: "Share App", systemImage: "square.and.arrow.up", accessory: nil, tapAction: #selector(didTapShareApp)),
1562 1580
             makeSettingsRow(title: "More Apps", systemImage: "square.grid.2x2", accessory: nil, tapAction: #selector(didTapMoreApps))
@@ -1580,6 +1598,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1580 1598
         aboutStack.alignment = .leading
1581 1599
         aboutStack.translatesAutoresizingMaskIntoConstraints = false
1582 1600
 
1601
+        contentStack.addArrangedSubview(appearanceStack)
1583 1602
         contentStack.addArrangedSubview(settingsSection)
1584 1603
         contentStack.addArrangedSubview(aboutStack)
1585 1604
         settingsPageContainer.addSubview(contentStack)
@@ -1588,6 +1607,8 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1588 1607
             contentStack.leadingAnchor.constraint(equalTo: settingsPageContainer.leadingAnchor, constant: 42),
1589 1608
             contentStack.trailingAnchor.constraint(lessThanOrEqualTo: settingsPageContainer.trailingAnchor, constant: -42),
1590 1609
             contentStack.topAnchor.constraint(equalTo: settingsPageContainer.topAnchor, constant: 48),
1610
+            appearanceStack.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
1611
+            appearanceSection.widthAnchor.constraint(equalTo: appearanceStack.widthAnchor),
1591 1612
             settingsSection.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
1592 1613
             aboutStack.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
1593 1614
             aboutSection.widthAnchor.constraint(equalTo: aboutStack.widthAnchor),
@@ -1595,6 +1616,26 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1595 1616
         ])
1596 1617
     }
1597 1618
 
1619
+    private func makeAppearanceModeSegment() -> NSSegmentedControl {
1620
+        let segment = NSSegmentedControl(
1621
+            labels: ["System", "Light", "Dark"],
1622
+            trackingMode: .selectOne,
1623
+            target: self,
1624
+            action: #selector(appearanceModeChanged(_:))
1625
+        )
1626
+        segment.translatesAutoresizingMaskIntoConstraints = false
1627
+        segment.segmentStyle = .automatic
1628
+        segment.selectedSegment = AppAppearanceManager.shared.mode.segmentIndex
1629
+        segment.setContentHuggingPriority(.required, for: .horizontal)
1630
+        segment.setContentCompressionResistancePriority(.required, for: .horizontal)
1631
+        return segment
1632
+    }
1633
+
1634
+    @objc private func appearanceModeChanged(_ sender: NSSegmentedControl) {
1635
+        guard let mode = AppAppearanceManager.Mode(segmentIndex: sender.selectedSegment) else { return }
1636
+        AppAppearanceManager.shared.mode = mode
1637
+    }
1638
+
1598 1639
     private func makeSettingsSection(rows: [NSView]) -> NSView {
1599 1640
         let section = NSStackView()
1600 1641
         section.orientation = .vertical