Pārlūkot izejas kodu

Apply dark mode across the app on launch and when toggled.

Add dynamic theme colors, live appearance refresh, premium page dark styling, and preserve Quick Start card colors in both modes.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 5 stundas atpakaļ
vecāks
revīzija
0ca58b3cd0

+ 4 - 1
smart_printer/AppDelegate.swift

@@ -10,8 +10,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
10
 
10
 
11
     private weak var mainWindowController: MainWindowController?
11
     private weak var mainWindowController: MainWindowController?
12
 
12
 
13
-    func applicationDidFinishLaunching(_ notification: Notification) {
13
+    func applicationWillFinishLaunching(_ notification: Notification) {
14
         AppSettings.applyAppearance()
14
         AppSettings.applyAppearance()
15
+    }
16
+
17
+    func applicationDidFinishLaunching(_ notification: Notification) {
15
         resolveMainWindowController()
18
         resolveMainWindowController()
16
         configureMainWindow()
19
         configureMainWindow()
17
         configurePreferencesMenu()
20
         configurePreferencesMenu()

+ 5 - 0
smart_printer/AppSettings.swift

@@ -148,6 +148,7 @@ enum AppSettings {
148
 
148
 
149
     static func applyAppearance() {
149
     static func applyAppearance() {
150
         NSApp.appearance = darkModeEnabled ? NSAppearance(named: .darkAqua) : NSAppearance(named: .aqua)
150
         NSApp.appearance = darkModeEnabled ? NSAppearance(named: .darkAqua) : NSAppearance(named: .aqua)
151
+        NotificationCenter.default.post(name: .appearanceDidChange, object: nil)
151
     }
152
     }
152
 
153
 
153
     static var appLanguage: String {
154
     static var appLanguage: String {
@@ -155,3 +156,7 @@ enum AppSettings {
155
         set { defaults.set(newValue, forKey: Key.appLanguage) }
156
         set { defaults.set(newValue, forKey: Key.appLanguage) }
156
     }
157
     }
157
 }
158
 }
159
+
160
+extension Notification.Name {
161
+    static let appearanceDidChange = Notification.Name("appearanceDidChange")
162
+}

+ 204 - 12
smart_printer/AppTheme.swift

@@ -1,4 +1,5 @@
1
 import Cocoa
1
 import Cocoa
2
+import ObjectiveC
2
 
3
 
3
 enum AppTheme {
4
 enum AppTheme {
4
     static let windowWidth: CGFloat = 760
5
     static let windowWidth: CGFloat = 760
@@ -29,19 +30,93 @@ enum AppTheme {
29
     static let cardCornerRadius: CGFloat = 20
30
     static let cardCornerRadius: CGFloat = 20
30
     static let featureCardCornerRadius: CGFloat = 16
31
     static let featureCardCornerRadius: CGFloat = 16
31
 
32
 
32
-    static let background = NSColor(calibratedWhite: 0.965, alpha: 1)
33
-    static let sidebarBackground = NSColor.white
34
-    static let cardBackground = NSColor.white
35
-    static let textPrimary = NSColor(calibratedWhite: 0.12, alpha: 1)
36
-    static let textSecondary = NSColor(calibratedWhite: 0.48, alpha: 1)
33
+    private static var isDark: Bool { AppSettings.darkModeEnabled }
37
 
34
 
38
-    static let homeActiveBackground = NSColor(red: 0.91, green: 0.94, blue: 1.0, alpha: 1)
39
-    static let homeActiveForeground = NSColor(red: 0.22, green: 0.47, blue: 0.96, alpha: 1)
40
-    static let premiumBackground = NSColor(red: 0.95, green: 0.93, blue: 1.0, alpha: 1)
41
-    static let premiumForeground = NSColor(red: 0.55, green: 0.36, blue: 0.96, alpha: 1)
35
+    static var background: NSColor {
36
+        isDark
37
+            ? NSColor(calibratedWhite: 0.11, alpha: 1)
38
+            : NSColor(calibratedWhite: 0.965, alpha: 1)
39
+    }
40
+
41
+    static var sidebarBackground: NSColor {
42
+        isDark
43
+            ? NSColor(calibratedWhite: 0.15, alpha: 1)
44
+            : NSColor.white
45
+    }
46
+
47
+    static var cardBackground: NSColor {
48
+        isDark
49
+            ? NSColor(calibratedWhite: 0.18, alpha: 1)
50
+            : NSColor.white
51
+    }
52
+
53
+    static var groupCardBackground: NSColor { cardBackground }
54
+
55
+    static var elevatedBackground: NSColor {
56
+        isDark
57
+            ? NSColor(calibratedWhite: 0.22, alpha: 1)
58
+            : NSColor.white
59
+    }
60
+
61
+    static var textPrimary: NSColor {
62
+        isDark
63
+            ? NSColor(calibratedWhite: 0.92, alpha: 1)
64
+            : NSColor(calibratedWhite: 0.12, alpha: 1)
65
+    }
66
+
67
+    static var textSecondary: NSColor {
68
+        isDark
69
+            ? NSColor(calibratedWhite: 0.55, alpha: 1)
70
+            : NSColor(calibratedWhite: 0.48, alpha: 1)
71
+    }
72
+
73
+    static var border: NSColor {
74
+        isDark
75
+            ? NSColor(calibratedWhite: 0.28, alpha: 1)
76
+            : NSColor(calibratedWhite: 0.92, alpha: 1)
77
+    }
78
+
79
+    static var waveDotColor: NSColor {
80
+        isDark
81
+            ? NSColor(calibratedWhite: 0.35, alpha: 0.45)
82
+            : NSColor(calibratedWhite: 0.82, alpha: 0.55)
83
+    }
84
+
85
+    static var homeActiveBackground: NSColor {
86
+        isDark
87
+            ? NSColor(red: 0.18, green: 0.28, blue: 0.48, alpha: 1)
88
+            : NSColor(red: 0.91, green: 0.94, blue: 1.0, alpha: 1)
89
+    }
90
+
91
+    static var homeActiveForeground: NSColor {
92
+        isDark
93
+            ? NSColor(red: 0.45, green: 0.65, blue: 1.0, alpha: 1)
94
+            : NSColor(red: 0.22, green: 0.47, blue: 0.96, alpha: 1)
95
+    }
96
+
97
+    static var premiumBackground: NSColor {
98
+        isDark
99
+            ? NSColor(red: 0.28, green: 0.22, blue: 0.42, alpha: 1)
100
+            : NSColor(red: 0.95, green: 0.93, blue: 1.0, alpha: 1)
101
+    }
102
+
103
+    static var premiumForeground: NSColor {
104
+        isDark
105
+            ? NSColor(red: 0.72, green: 0.58, blue: 1.0, alpha: 1)
106
+            : NSColor(red: 0.55, green: 0.36, blue: 0.96, alpha: 1)
107
+    }
42
 
108
 
43
     static let blue = NSColor(red: 0.22, green: 0.47, blue: 0.96, alpha: 1)
109
     static let blue = NSColor(red: 0.22, green: 0.47, blue: 0.96, alpha: 1)
44
-    static let blueLight = NSColor(red: 0.88, green: 0.93, blue: 1.0, alpha: 1)
110
+    static let quickStartBlueLight = NSColor(red: 0.88, green: 0.93, blue: 1.0, alpha: 1)
111
+    static let quickStartCardSubtitle = NSColor(calibratedWhite: 0.48, alpha: 1)
112
+    static let quickStartWaveDot = NSColor(calibratedWhite: 0.82, alpha: 0.55)
113
+
114
+    static var blueLight: NSColor {
115
+        isDark
116
+            ? NSColor(red: 0.20, green: 0.28, blue: 0.42, alpha: 1)
117
+            : quickStartBlueLight
118
+    }
119
+
45
     static let green = NSColor(red: 0.13, green: 0.68, blue: 0.42, alpha: 1)
120
     static let green = NSColor(red: 0.13, green: 0.68, blue: 0.42, alpha: 1)
46
     static let greenLight = NSColor(red: 0.88, green: 0.97, blue: 0.91, alpha: 1)
121
     static let greenLight = NSColor(red: 0.88, green: 0.97, blue: 0.91, alpha: 1)
47
     static let orange = NSColor(red: 0.96, green: 0.52, blue: 0.18, alpha: 1)
122
     static let orange = NSColor(red: 0.96, green: 0.52, blue: 0.18, alpha: 1)
@@ -53,7 +128,58 @@ enum AppTheme {
53
     static let paywallPinkText = NSColor(red: 0.75, green: 0.30, blue: 0.45, alpha: 1)
128
     static let paywallPinkText = NSColor(red: 0.75, green: 0.30, blue: 0.45, alpha: 1)
54
     static let paywallGold = NSColor(red: 0.96, green: 0.90, blue: 0.78, alpha: 1)
129
     static let paywallGold = NSColor(red: 0.96, green: 0.90, blue: 0.78, alpha: 1)
55
     static let paywallGoldText = NSColor(red: 0.65, green: 0.48, blue: 0.22, alpha: 1)
130
     static let paywallGoldText = NSColor(red: 0.65, green: 0.48, blue: 0.22, alpha: 1)
56
-    static let paywallBorder = NSColor(calibratedWhite: 0.88, alpha: 1)
131
+
132
+    static var paywallBorder: NSColor { border }
133
+
134
+    static var paywallBackground: NSColor { cardBackground }
135
+
136
+    static var paywallAccent: NSColor {
137
+        isDark ? textPrimary : navy
138
+    }
139
+
140
+    static var paywallLeftGradientColors: [NSColor] {
141
+        isDark
142
+            ? [
143
+                NSColor(calibratedWhite: 0.17, alpha: 1),
144
+                NSColor(calibratedWhite: 0.13, alpha: 1),
145
+            ]
146
+            : [
147
+                NSColor(red: 0.88, green: 0.94, blue: 1.0, alpha: 1),
148
+                NSColor(red: 0.95, green: 0.97, blue: 1.0, alpha: 1),
149
+            ]
150
+    }
151
+
152
+    static var paywallCTABackground: NSColor {
153
+        isDark
154
+            ? NSColor(calibratedWhite: 0.96, alpha: 1)
155
+            : navy
156
+    }
157
+
158
+    static var paywallCTAForeground: NSColor {
159
+        isDark
160
+            ? NSColor(calibratedWhite: 0.10, alpha: 1)
161
+            : NSColor.white
162
+    }
163
+
164
+    static var paywallTrustIconBackground: NSColor {
165
+        isDark
166
+            ? NSColor(calibratedWhite: 0.24, alpha: 1)
167
+            : quickStartBlueLight
168
+    }
169
+
170
+    static var paywallIconAccent: NSColor {
171
+        isDark
172
+            ? NSColor(calibratedWhite: 0.78, alpha: 1)
173
+            : navy
174
+    }
175
+
176
+    static var paywallTrustBackground: NSColor { elevatedBackground }
177
+
178
+    static var paywallOverlayBackdrop: NSColor {
179
+        isDark
180
+            ? NSColor(calibratedWhite: 0.05, alpha: 0.55)
181
+            : NSColor(calibratedWhite: 0.15, alpha: 0.22)
182
+    }
57
 
183
 
58
     static func semiboldFont(size: CGFloat) -> NSFont {
184
     static func semiboldFont(size: CGFloat) -> NSFont {
59
         .systemFont(ofSize: size, weight: .semibold)
185
         .systemFont(ofSize: size, weight: .semibold)
@@ -68,11 +194,15 @@ enum AppTheme {
68
     }
194
     }
69
 }
195
 }
70
 
196
 
197
+protocol AppearanceRefreshable: AnyObject {
198
+    func refreshAppearance()
199
+}
200
+
71
 extension NSView {
201
 extension NSView {
72
     func applyCardShadow() {
202
     func applyCardShadow() {
73
         wantsLayer = true
203
         wantsLayer = true
74
         layer?.shadowColor = NSColor.black.cgColor
204
         layer?.shadowColor = NSColor.black.cgColor
75
-        layer?.shadowOpacity = 0.07
205
+        layer?.shadowOpacity = AppSettings.darkModeEnabled ? 0.25 : 0.07
76
         layer?.shadowOffset = NSSize(width: 0, height: -3)
206
         layer?.shadowOffset = NSSize(width: 0, height: -3)
77
         layer?.shadowRadius = 14
207
         layer?.shadowRadius = 14
78
         layer?.masksToBounds = false
208
         layer?.masksToBounds = false
@@ -83,4 +213,66 @@ extension NSView {
83
         layer?.cornerRadius = radius
213
         layer?.cornerRadius = radius
84
         layer?.masksToBounds = true
214
         layer?.masksToBounds = true
85
     }
215
     }
216
+
217
+    func refreshAppearanceRecursively() {
218
+        if let refreshable = self as? AppearanceRefreshable {
219
+            refreshable.refreshAppearance()
220
+        }
221
+        if let label = self as? NSTextField {
222
+            label.refreshThemeLabelColor()
223
+        }
224
+        if let imageView = self as? NSImageView, imageView.usesThemeTint {
225
+            imageView.contentTintColor = AppTheme.textPrimary
226
+        }
227
+        subviews.forEach { $0.refreshAppearanceRecursively() }
228
+    }
229
+}
230
+
231
+private enum ThemeAssociatedKeys {
232
+    static var labelStyle = 0
233
+    static var usesThemeTint = 0
234
+}
235
+
236
+enum ThemeLabelStyle: String {
237
+    case primary
238
+    case secondary
239
+}
240
+
241
+extension NSTextField {
242
+    static func themeLabel(_ string: String, style: ThemeLabelStyle, font: NSFont) -> NSTextField {
243
+        let label = NSTextField(labelWithString: string)
244
+        label.font = font
245
+        label.themeLabelStyle = style
246
+        label.textColor = style == .primary ? AppTheme.textPrimary : AppTheme.textSecondary
247
+        return label
248
+    }
249
+
250
+    var themeLabelStyle: ThemeLabelStyle? {
251
+        get {
252
+            guard let rawValue = objc_getAssociatedObject(self, &ThemeAssociatedKeys.labelStyle) as? String else {
253
+                return nil
254
+            }
255
+            return ThemeLabelStyle(rawValue: rawValue)
256
+        }
257
+        set {
258
+            objc_setAssociatedObject(
259
+                self,
260
+                &ThemeAssociatedKeys.labelStyle,
261
+                newValue?.rawValue,
262
+                .OBJC_ASSOCIATION_RETAIN_NONATOMIC
263
+            )
264
+        }
265
+    }
266
+
267
+    func refreshThemeLabelColor() {
268
+        guard let style = themeLabelStyle else { return }
269
+        textColor = style == .primary ? AppTheme.textPrimary : AppTheme.textSecondary
270
+    }
271
+}
272
+
273
+extension NSImageView {
274
+    var usesThemeTint: Bool {
275
+        get { (objc_getAssociatedObject(self, &ThemeAssociatedKeys.usesThemeTint) as? Bool) ?? false }
276
+        set { objc_setAssociatedObject(self, &ThemeAssociatedKeys.usesThemeTint, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
277
+    }
86
 }
278
 }

+ 88 - 42
smart_printer/PaywallView.swift

@@ -42,19 +42,20 @@ enum PaywallPlan: CaseIterable {
42
 
42
 
43
 // MARK: - Left Panel
43
 // MARK: - Left Panel
44
 
44
 
45
-private final class PaywallLeftPanelView: NSView {
45
+private final class PaywallLeftPanelView: NSView, AppearanceRefreshable {
46
     private let gradientLayer = CAGradientLayer()
46
     private let gradientLayer = CAGradientLayer()
47
 
47
 
48
     override init(frame frameRect: NSRect) {
48
     override init(frame frameRect: NSRect) {
49
         super.init(frame: frameRect)
49
         super.init(frame: frameRect)
50
         wantsLayer = true
50
         wantsLayer = true
51
-        gradientLayer.colors = [
52
-            NSColor(red: 0.88, green: 0.94, blue: 1.0, alpha: 1).cgColor,
53
-            NSColor(red: 0.95, green: 0.97, blue: 1.0, alpha: 1).cgColor,
54
-        ]
55
         gradientLayer.startPoint = CGPoint(x: 0.5, y: 1)
51
         gradientLayer.startPoint = CGPoint(x: 0.5, y: 1)
56
         gradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
52
         gradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
57
         layer?.insertSublayer(gradientLayer, at: 0)
53
         layer?.insertSublayer(gradientLayer, at: 0)
54
+        refreshAppearance()
55
+    }
56
+
57
+    func refreshAppearance() {
58
+        gradientLayer.colors = AppTheme.paywallLeftGradientColors.map(\.cgColor)
58
     }
59
     }
59
 
60
 
60
     @available(*, unavailable)
61
     @available(*, unavailable)
@@ -121,8 +122,11 @@ private final class PaywallBadgeView: NSView {
121
 
122
 
122
 // MARK: - Feature Row
123
 // MARK: - Feature Row
123
 
124
 
124
-private final class PaywallFeatureRow: NSView {
125
+private final class PaywallFeatureRow: NSView, AppearanceRefreshable {
126
+    private let label: NSTextField
127
+
125
     init(text: String) {
128
     init(text: String) {
129
+        label = NSTextField.themeLabel(text, style: .primary, font: AppTheme.regularFont(size: 14))
126
         super.init(frame: .zero)
130
         super.init(frame: .zero)
127
         translatesAutoresizingMaskIntoConstraints = false
131
         translatesAutoresizingMaskIntoConstraints = false
128
 
132
 
@@ -141,9 +145,6 @@ private final class PaywallFeatureRow: NSView {
141
         }
145
         }
142
         checkIcon.contentTintColor = .white
146
         checkIcon.contentTintColor = .white
143
 
147
 
144
-        let label = NSTextField(labelWithString: text)
145
-        label.font = AppTheme.regularFont(size: 14)
146
-        label.textColor = AppTheme.navy
147
         label.translatesAutoresizingMaskIntoConstraints = false
148
         label.translatesAutoresizingMaskIntoConstraints = false
148
 
149
 
149
         addSubview(checkContainer)
150
         addSubview(checkContainer)
@@ -171,11 +172,15 @@ private final class PaywallFeatureRow: NSView {
171
 
172
 
172
     @available(*, unavailable)
173
     @available(*, unavailable)
173
     required init?(coder: NSCoder) { nil }
174
     required init?(coder: NSCoder) { nil }
175
+
176
+    func refreshAppearance() {
177
+        label.refreshThemeLabelColor()
178
+    }
174
 }
179
 }
175
 
180
 
176
 // MARK: - Plan Card
181
 // MARK: - Plan Card
177
 
182
 
178
-private final class PaywallPlanCard: NSControl {
183
+private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
179
     var onSelect: (() -> Void)?
184
     var onSelect: (() -> Void)?
180
 
185
 
181
     private let plan: PaywallPlan
186
     private let plan: PaywallPlan
@@ -194,7 +199,6 @@ private final class PaywallPlanCard: NSControl {
194
         translatesAutoresizingMaskIntoConstraints = false
199
         translatesAutoresizingMaskIntoConstraints = false
195
         wantsLayer = true
200
         wantsLayer = true
196
         layer?.cornerRadius = 12
201
         layer?.cornerRadius = 12
197
-        layer?.backgroundColor = NSColor.white.cgColor
198
         layer?.masksToBounds = false
202
         layer?.masksToBounds = false
199
 
203
 
200
         titleLabel.stringValue = plan.title
204
         titleLabel.stringValue = plan.title
@@ -203,6 +207,7 @@ private final class PaywallPlanCard: NSControl {
203
 
207
 
204
         subtitleLabel.stringValue = plan.subtitle
208
         subtitleLabel.stringValue = plan.subtitle
205
         subtitleLabel.font = AppTheme.regularFont(size: 11)
209
         subtitleLabel.font = AppTheme.regularFont(size: 11)
210
+        subtitleLabel.themeLabelStyle = .secondary
206
         subtitleLabel.textColor = AppTheme.textSecondary
211
         subtitleLabel.textColor = AppTheme.textSecondary
207
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
212
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
208
 
213
 
@@ -263,11 +268,17 @@ private final class PaywallPlanCard: NSControl {
263
     @available(*, unavailable)
268
     @available(*, unavailable)
264
     required init?(coder: NSCoder) { nil }
269
     required init?(coder: NSCoder) { nil }
265
 
270
 
271
+    func refreshAppearance() {
272
+        updateAppearance()
273
+        subtitleLabel.refreshThemeLabelColor()
274
+    }
275
+
266
     private func updateAppearance() {
276
     private func updateAppearance() {
267
-        let titleColor = isChosen && plan == .yearly ? AppTheme.green : AppTheme.navy
277
+        let titleColor = isChosen && plan == .yearly ? AppTheme.green : AppTheme.paywallAccent
268
         titleLabel.textColor = titleColor
278
         titleLabel.textColor = titleColor
269
         priceLabel.textColor = titleColor
279
         priceLabel.textColor = titleColor
270
 
280
 
281
+        layer?.backgroundColor = AppTheme.paywallTrustBackground.cgColor
271
         layer?.borderWidth = isChosen ? 2 : 1
282
         layer?.borderWidth = isChosen ? 2 : 1
272
         layer?.borderColor = (isChosen ? AppTheme.green : AppTheme.paywallBorder).cgColor
283
         layer?.borderColor = (isChosen ? AppTheme.green : AppTheme.paywallBorder).cgColor
273
     }
284
     }
@@ -284,19 +295,23 @@ private final class PaywallPlanCard: NSControl {
284
 
295
 
285
 // MARK: - Footer Link
296
 // MARK: - Footer Link
286
 
297
 
287
-private final class PaywallFooterLink: NSButton {
298
+private final class PaywallFooterLink: NSButton, AppearanceRefreshable {
288
     init(title: String) {
299
     init(title: String) {
289
         super.init(frame: .zero)
300
         super.init(frame: .zero)
290
         self.title = title
301
         self.title = title
291
         isBordered = false
302
         isBordered = false
292
         font = AppTheme.regularFont(size: 11)
303
         font = AppTheme.regularFont(size: 11)
293
-        contentTintColor = AppTheme.textSecondary
294
         translatesAutoresizingMaskIntoConstraints = false
304
         translatesAutoresizingMaskIntoConstraints = false
305
+        refreshAppearance()
295
     }
306
     }
296
 
307
 
297
     @available(*, unavailable)
308
     @available(*, unavailable)
298
     required init?(coder: NSCoder) { nil }
309
     required init?(coder: NSCoder) { nil }
299
 
310
 
311
+    func refreshAppearance() {
312
+        contentTintColor = AppTheme.textSecondary
313
+    }
314
+
300
     override func resetCursorRects() {
315
     override func resetCursorRects() {
301
         addCursorRect(bounds, cursor: .pointingHand)
316
         addCursorRect(bounds, cursor: .pointingHand)
302
     }
317
     }
@@ -304,36 +319,33 @@ private final class PaywallFooterLink: NSButton {
304
 
319
 
305
 // MARK: - Footer Trust Item
320
 // MARK: - Footer Trust Item
306
 
321
 
307
-private final class PaywallTrustItemView: NSView {
322
+private final class PaywallTrustItemView: NSView, AppearanceRefreshable {
323
+    private let iconContainer = NSView()
324
+    private let icon = NSImageView()
325
+    private let titleLabel: NSTextField
326
+    private let subtitleLabel: NSTextField
327
+
308
     init(iconName: String, title: String, subtitle: String) {
328
     init(iconName: String, title: String, subtitle: String) {
329
+        titleLabel = NSTextField.themeLabel(title, style: .primary, font: AppTheme.semiboldFont(size: 11))
330
+        subtitleLabel = NSTextField.themeLabel(subtitle, style: .secondary, font: AppTheme.regularFont(size: 9))
309
         super.init(frame: .zero)
331
         super.init(frame: .zero)
310
         translatesAutoresizingMaskIntoConstraints = false
332
         translatesAutoresizingMaskIntoConstraints = false
311
 
333
 
312
-        let iconContainer = NSView()
313
         iconContainer.translatesAutoresizingMaskIntoConstraints = false
334
         iconContainer.translatesAutoresizingMaskIntoConstraints = false
314
         iconContainer.wantsLayer = true
335
         iconContainer.wantsLayer = true
315
-        iconContainer.layer?.backgroundColor = AppTheme.blueLight.cgColor
316
         iconContainer.layer?.cornerRadius = 10
336
         iconContainer.layer?.cornerRadius = 10
317
         iconContainer.layer?.masksToBounds = true
337
         iconContainer.layer?.masksToBounds = true
318
 
338
 
319
-        let icon = NSImageView()
320
         icon.translatesAutoresizingMaskIntoConstraints = false
339
         icon.translatesAutoresizingMaskIntoConstraints = false
321
         if let image = NSImage(systemSymbolName: iconName, accessibilityDescription: nil) {
340
         if let image = NSImage(systemSymbolName: iconName, accessibilityDescription: nil) {
322
             let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
341
             let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
323
             icon.image = image.withSymbolConfiguration(config)
342
             icon.image = image.withSymbolConfiguration(config)
324
         }
343
         }
325
-        icon.contentTintColor = AppTheme.navy
326
 
344
 
327
-        let titleLabel = NSTextField(labelWithString: title)
328
-        titleLabel.font = AppTheme.semiboldFont(size: 11)
329
-        titleLabel.textColor = AppTheme.navy
330
         titleLabel.lineBreakMode = .byTruncatingTail
345
         titleLabel.lineBreakMode = .byTruncatingTail
331
         titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
346
         titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
332
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
347
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
333
 
348
 
334
-        let subtitleLabel = NSTextField(labelWithString: subtitle)
335
-        subtitleLabel.font = AppTheme.regularFont(size: 9)
336
-        subtitleLabel.textColor = AppTheme.textSecondary
337
         subtitleLabel.lineBreakMode = .byTruncatingTail
349
         subtitleLabel.lineBreakMode = .byTruncatingTail
338
         subtitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
350
         subtitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
339
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
351
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
@@ -366,15 +378,23 @@ private final class PaywallTrustItemView: NSView {
366
 
378
 
367
         setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
379
         setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
368
         setContentHuggingPriority(.defaultLow, for: .horizontal)
380
         setContentHuggingPriority(.defaultLow, for: .horizontal)
381
+        refreshAppearance()
369
     }
382
     }
370
 
383
 
371
     @available(*, unavailable)
384
     @available(*, unavailable)
372
     required init?(coder: NSCoder) { nil }
385
     required init?(coder: NSCoder) { nil }
386
+
387
+    func refreshAppearance() {
388
+        iconContainer.layer?.backgroundColor = AppTheme.paywallTrustIconBackground.cgColor
389
+        icon.contentTintColor = AppTheme.paywallIconAccent
390
+        titleLabel.refreshThemeLabelColor()
391
+        subtitleLabel.refreshThemeLabelColor()
392
+    }
373
 }
393
 }
374
 
394
 
375
 // MARK: - Main Paywall Card
395
 // MARK: - Main Paywall Card
376
 
396
 
377
-final class PaywallView: NSView {
397
+final class PaywallView: NSView, AppearanceRefreshable {
378
     var onClose: (() -> Void)?
398
     var onClose: (() -> Void)?
379
     var onPurchase: ((PaywallPlan) -> Void)?
399
     var onPurchase: ((PaywallPlan) -> Void)?
380
     var onRestore: (() -> Void)?
400
     var onRestore: (() -> Void)?
@@ -382,14 +402,30 @@ final class PaywallView: NSView {
382
     private var selectedPlan: PaywallPlan = .yearly
402
     private var selectedPlan: PaywallPlan = .yearly
383
     private var planCards: [PaywallPlan: PaywallPlanCard] = [:]
403
     private var planCards: [PaywallPlan: PaywallPlanCard] = [:]
384
     private let ctaButton = NSButton()
404
     private let ctaButton = NSButton()
405
+    private var leftPanelTitle: NSTextField!
406
+    private var rightTitle: NSTextField!
407
+    private var rightSubtitle: NSTextField!
408
+    private var trustStack: NSStackView!
385
 
409
 
386
     init() {
410
     init() {
387
         super.init(frame: .zero)
411
         super.init(frame: .zero)
388
         translatesAutoresizingMaskIntoConstraints = false
412
         translatesAutoresizingMaskIntoConstraints = false
389
         wantsLayer = true
413
         wantsLayer = true
390
-        layer?.backgroundColor = NSColor.white.cgColor
391
         layer?.cornerRadius = 0
414
         layer?.cornerRadius = 0
392
         setup()
415
         setup()
416
+        refreshAppearance()
417
+    }
418
+
419
+    func refreshAppearance() {
420
+        layer?.backgroundColor = AppTheme.paywallBackground.cgColor
421
+        leftPanelTitle?.refreshThemeLabelColor()
422
+        rightTitle?.refreshThemeLabelColor()
423
+        rightSubtitle?.refreshThemeLabelColor()
424
+        trustStack?.layer?.backgroundColor = AppTheme.paywallTrustBackground.cgColor
425
+        trustStack?.layer?.borderColor = AppTheme.paywallBorder.cgColor
426
+        ctaButton.layer?.backgroundColor = AppTheme.paywallCTABackground.cgColor
427
+        ctaButton.contentTintColor = AppTheme.paywallCTAForeground
428
+        subviews.forEach { $0.refreshAppearanceRecursively() }
393
     }
429
     }
394
 
430
 
395
     @available(*, unavailable)
431
     @available(*, unavailable)
@@ -419,11 +455,14 @@ final class PaywallView: NSView {
419
         let panel = PaywallLeftPanelView()
455
         let panel = PaywallLeftPanelView()
420
         panel.translatesAutoresizingMaskIntoConstraints = false
456
         panel.translatesAutoresizingMaskIntoConstraints = false
421
 
457
 
422
-        let title = NSTextField(labelWithString: "Unlock Your Full\nPrinting Potential")
423
-        title.font = AppTheme.semiboldFont(size: 22)
424
-        title.textColor = AppTheme.navy
458
+        let title = NSTextField.themeLabel(
459
+            "Unlock Your Full\nPrinting Potential",
460
+            style: .primary,
461
+            font: AppTheme.semiboldFont(size: 22)
462
+        )
425
         title.maximumNumberOfLines = 2
463
         title.maximumNumberOfLines = 2
426
         title.translatesAutoresizingMaskIntoConstraints = false
464
         title.translatesAutoresizingMaskIntoConstraints = false
465
+        leftPanelTitle = title
427
 
466
 
428
         let featuresStack = NSStackView()
467
         let featuresStack = NSStackView()
429
         featuresStack.orientation = .vertical
468
         featuresStack.orientation = .vertical
@@ -463,18 +502,20 @@ final class PaywallView: NSView {
463
         let panel = NSView()
502
         let panel = NSView()
464
         panel.translatesAutoresizingMaskIntoConstraints = false
503
         panel.translatesAutoresizingMaskIntoConstraints = false
465
 
504
 
466
-        let title = NSTextField(labelWithString: "Go Premium")
467
-        title.font = AppTheme.semiboldFont(size: 26)
468
-        title.textColor = AppTheme.navy
505
+        let title = NSTextField.themeLabel("Go Premium", style: .primary, font: AppTheme.semiboldFont(size: 26))
469
         title.alignment = .center
506
         title.alignment = .center
470
         title.translatesAutoresizingMaskIntoConstraints = false
507
         title.translatesAutoresizingMaskIntoConstraints = false
508
+        rightTitle = title
471
 
509
 
472
-        let subtitle = NSTextField(labelWithString: "Experience professional quality printing and scanning without limits.")
473
-        subtitle.font = AppTheme.regularFont(size: 13)
474
-        subtitle.textColor = AppTheme.textSecondary
510
+        let subtitle = NSTextField.themeLabel(
511
+            "Experience professional quality printing and scanning without limits.",
512
+            style: .secondary,
513
+            font: AppTheme.regularFont(size: 13)
514
+        )
475
         subtitle.alignment = .center
515
         subtitle.alignment = .center
476
         subtitle.maximumNumberOfLines = 2
516
         subtitle.maximumNumberOfLines = 2
477
         subtitle.translatesAutoresizingMaskIntoConstraints = false
517
         subtitle.translatesAutoresizingMaskIntoConstraints = false
518
+        rightSubtitle = subtitle
478
 
519
 
479
         let plansStack = NSStackView()
520
         let plansStack = NSStackView()
480
         plansStack.orientation = .vertical
521
         plansStack.orientation = .vertical
@@ -492,10 +533,8 @@ final class PaywallView: NSView {
492
         ctaButton.title = selectedPlan.ctaTitle
533
         ctaButton.title = selectedPlan.ctaTitle
493
         ctaButton.isBordered = false
534
         ctaButton.isBordered = false
494
         ctaButton.wantsLayer = true
535
         ctaButton.wantsLayer = true
495
-        ctaButton.layer?.backgroundColor = AppTheme.navy.cgColor
496
         ctaButton.layer?.cornerRadius = 12
536
         ctaButton.layer?.cornerRadius = 12
497
         ctaButton.font = AppTheme.semiboldFont(size: 15)
537
         ctaButton.font = AppTheme.semiboldFont(size: 15)
498
-        ctaButton.contentTintColor = .white
499
         ctaButton.target = self
538
         ctaButton.target = self
500
         ctaButton.action = #selector(purchaseTapped)
539
         ctaButton.action = #selector(purchaseTapped)
501
         ctaButton.translatesAutoresizingMaskIntoConstraints = false
540
         ctaButton.translatesAutoresizingMaskIntoConstraints = false
@@ -569,13 +608,12 @@ final class PaywallView: NSView {
569
         trustStack.alignment = .top
608
         trustStack.alignment = .top
570
         trustStack.translatesAutoresizingMaskIntoConstraints = false
609
         trustStack.translatesAutoresizingMaskIntoConstraints = false
571
         trustStack.wantsLayer = true
610
         trustStack.wantsLayer = true
572
-        trustStack.layer?.backgroundColor = NSColor.white.cgColor
573
         trustStack.layer?.cornerRadius = 12
611
         trustStack.layer?.cornerRadius = 12
574
         trustStack.layer?.borderWidth = 1
612
         trustStack.layer?.borderWidth = 1
575
-        trustStack.layer?.borderColor = AppTheme.paywallBorder.cgColor
576
         trustStack.layer?.masksToBounds = true
613
         trustStack.layer?.masksToBounds = true
577
         trustStack.edgeInsets = NSEdgeInsets(top: 12, left: 14, bottom: 12, right: 14)
614
         trustStack.edgeInsets = NSEdgeInsets(top: 12, left: 14, bottom: 12, right: 14)
578
         trustStack.translatesAutoresizingMaskIntoConstraints = false
615
         trustStack.translatesAutoresizingMaskIntoConstraints = false
616
+        self.trustStack = trustStack
579
 
617
 
580
         return trustStack
618
         return trustStack
581
     }
619
     }
@@ -657,18 +695,27 @@ final class PaywallView: NSView {
657
 
695
 
658
 // MARK: - Overlay Presenter
696
 // MARK: - Overlay Presenter
659
 
697
 
660
-final class PaywallOverlayView: NSView {
698
+final class PaywallOverlayView: NSView, AppearanceRefreshable {
661
     var onDismiss: (() -> Void)?
699
     var onDismiss: (() -> Void)?
662
 
700
 
663
     private let paywallView: PaywallView
701
     private let paywallView: PaywallView
664
     private let blurView = NSVisualEffectView()
702
     private let blurView = NSVisualEffectView()
665
     private let backdrop = NSView()
703
     private let backdrop = NSView()
704
+    private let pattern = WavePatternView()
666
 
705
 
667
     init() {
706
     init() {
668
         paywallView = PaywallView()
707
         paywallView = PaywallView()
669
         super.init(frame: .zero)
708
         super.init(frame: .zero)
670
         translatesAutoresizingMaskIntoConstraints = false
709
         translatesAutoresizingMaskIntoConstraints = false
671
         setup()
710
         setup()
711
+        refreshAppearance()
712
+    }
713
+
714
+    func refreshAppearance() {
715
+        backdrop.layer?.backgroundColor = AppTheme.paywallOverlayBackdrop.cgColor
716
+        blurView.material = AppSettings.darkModeEnabled ? .hudWindow : .underWindowBackground
717
+        pattern.refreshAppearance()
718
+        paywallView.refreshAppearance()
672
     }
719
     }
673
 
720
 
674
     @available(*, unavailable)
721
     @available(*, unavailable)
@@ -684,7 +731,6 @@ final class PaywallOverlayView: NSView {
684
         backdrop.wantsLayer = true
731
         backdrop.wantsLayer = true
685
         backdrop.layer?.backgroundColor = NSColor(calibratedWhite: 0.15, alpha: 0.22).cgColor
732
         backdrop.layer?.backgroundColor = NSColor(calibratedWhite: 0.15, alpha: 0.22).cgColor
686
 
733
 
687
-        let pattern = WavePatternView()
688
         pattern.translatesAutoresizingMaskIntoConstraints = false
734
         pattern.translatesAutoresizingMaskIntoConstraints = false
689
         pattern.alphaValue = 0.35
735
         pattern.alphaValue = 0.35
690
 
736
 

+ 28 - 18
smart_printer/SettingsView.swift

@@ -2,7 +2,7 @@ import Cocoa
2
 
2
 
3
 // MARK: - Root
3
 // MARK: - Root
4
 
4
 
5
-final class SettingsView: NSView {
5
+final class SettingsView: NSView, AppearanceRefreshable {
6
     init() {
6
     init() {
7
         super.init(frame: .zero)
7
         super.init(frame: .zero)
8
         wantsLayer = true
8
         wantsLayer = true
@@ -14,6 +14,10 @@ final class SettingsView: NSView {
14
     @available(*, unavailable)
14
     @available(*, unavailable)
15
     required init?(coder: NSCoder) { nil }
15
     required init?(coder: NSCoder) { nil }
16
 
16
 
17
+    func refreshAppearance() {
18
+        layer?.backgroundColor = AppTheme.background.cgColor
19
+    }
20
+
17
     private func setup() {
21
     private func setup() {
18
         let scrollView = NSScrollView()
22
         let scrollView = NSScrollView()
19
         scrollView.translatesAutoresizingMaskIntoConstraints = false
23
         scrollView.translatesAutoresizingMaskIntoConstraints = false
@@ -81,9 +85,7 @@ final class SettingsView: NSView {
81
         container.spacing = 12
85
         container.spacing = 12
82
         container.translatesAutoresizingMaskIntoConstraints = false
86
         container.translatesAutoresizingMaskIntoConstraints = false
83
 
87
 
84
-        let label = NSTextField(labelWithString: title)
85
-        label.font = AppTheme.semiboldFont(size: 18)
86
-        label.textColor = AppTheme.textPrimary
88
+        let label = NSTextField.themeLabel(title, style: .primary, font: AppTheme.semiboldFont(size: 18))
87
         label.translatesAutoresizingMaskIntoConstraints = false
89
         label.translatesAutoresizingMaskIntoConstraints = false
88
 
90
 
89
         container.addArrangedSubview(label)
91
         container.addArrangedSubview(label)
@@ -137,32 +139,35 @@ final class SettingsView: NSView {
137
 
139
 
138
 // MARK: - Panel
140
 // MARK: - Panel
139
 
141
 
140
-private final class SettingsPanelView: NSView {
142
+private final class SettingsPanelView: NSView, AppearanceRefreshable {
141
     init() {
143
     init() {
142
         super.init(frame: .zero)
144
         super.init(frame: .zero)
143
         wantsLayer = true
145
         wantsLayer = true
144
-        layer?.backgroundColor = AppTheme.cardBackground.cgColor
145
         layer?.cornerRadius = 22
146
         layer?.cornerRadius = 22
146
         layer?.borderWidth = 1
147
         layer?.borderWidth = 1
147
-        layer?.borderColor = NSColor(calibratedWhite: 0.92, alpha: 1).cgColor
148
         applyCardShadow()
148
         applyCardShadow()
149
+        refreshAppearance()
150
+    }
151
+
152
+    func refreshAppearance() {
153
+        layer?.backgroundColor = AppTheme.cardBackground.cgColor
154
+        layer?.borderColor = AppTheme.border.cgColor
149
     }
155
     }
150
 
156
 
151
     @available(*, unavailable)
157
     @available(*, unavailable)
152
     required init?(coder: NSCoder) { nil }
158
     required init?(coder: NSCoder) { nil }
153
 }
159
 }
154
 
160
 
155
-private final class SettingsGroupCard: NSView {
161
+private final class SettingsGroupCard: NSView, AppearanceRefreshable {
156
     private let stack = NSStackView()
162
     private let stack = NSStackView()
157
 
163
 
158
     init() {
164
     init() {
159
         super.init(frame: .zero)
165
         super.init(frame: .zero)
160
         translatesAutoresizingMaskIntoConstraints = false
166
         translatesAutoresizingMaskIntoConstraints = false
161
         wantsLayer = true
167
         wantsLayer = true
162
-        layer?.backgroundColor = NSColor.white.cgColor
163
         layer?.cornerRadius = 16
168
         layer?.cornerRadius = 16
164
         layer?.borderWidth = 1
169
         layer?.borderWidth = 1
165
-        layer?.borderColor = NSColor(calibratedWhite: 0.93, alpha: 1).cgColor
170
+        refreshAppearance()
166
 
171
 
167
         stack.orientation = .vertical
172
         stack.orientation = .vertical
168
         stack.spacing = 0
173
         stack.spacing = 0
@@ -184,17 +189,22 @@ private final class SettingsGroupCard: NSView {
184
         stack.addArrangedSubview(row)
189
         stack.addArrangedSubview(row)
185
         row.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
190
         row.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
186
     }
191
     }
192
+
193
+    func refreshAppearance() {
194
+        layer?.backgroundColor = AppTheme.groupCardBackground.cgColor
195
+        layer?.borderColor = AppTheme.border.cgColor
196
+    }
187
 }
197
 }
188
 
198
 
189
 // MARK: - Icon
199
 // MARK: - Icon
190
 
200
 
191
-private final class SettingsIconBadge: NSView {
201
+private final class SettingsIconBadge: NSView, AppearanceRefreshable {
192
     init(symbolName: String) {
202
     init(symbolName: String) {
193
         super.init(frame: .zero)
203
         super.init(frame: .zero)
194
         translatesAutoresizingMaskIntoConstraints = false
204
         translatesAutoresizingMaskIntoConstraints = false
195
         wantsLayer = true
205
         wantsLayer = true
196
-        layer?.backgroundColor = AppTheme.blueLight.cgColor
197
         layer?.cornerRadius = 10
206
         layer?.cornerRadius = 10
207
+        refreshAppearance()
198
 
208
 
199
         let icon = NSImageView()
209
         let icon = NSImageView()
200
         icon.translatesAutoresizingMaskIntoConstraints = false
210
         icon.translatesAutoresizingMaskIntoConstraints = false
@@ -217,6 +227,10 @@ private final class SettingsIconBadge: NSView {
217
 
227
 
218
     @available(*, unavailable)
228
     @available(*, unavailable)
219
     required init?(coder: NSCoder) { nil }
229
     required init?(coder: NSCoder) { nil }
230
+
231
+    func refreshAppearance() {
232
+        layer?.backgroundColor = AppTheme.blueLight.cgColor
233
+    }
220
 }
234
 }
221
 
235
 
222
 // MARK: - Rows
236
 // MARK: - Rows
@@ -247,9 +261,7 @@ private class SettingsRowBase: NSView {
247
 
261
 
248
     func install(icon symbolName: String, title: String, trailing: NSView) -> NSTextField {
262
     func install(icon symbolName: String, title: String, trailing: NSView) -> NSTextField {
249
         let badge = SettingsIconBadge(symbolName: symbolName)
263
         let badge = SettingsIconBadge(symbolName: symbolName)
250
-        let titleLabel = NSTextField(labelWithString: title)
251
-        titleLabel.font = AppTheme.mediumFont(size: 15)
252
-        titleLabel.textColor = AppTheme.textPrimary
264
+        let titleLabel = NSTextField.themeLabel(title, style: .primary, font: AppTheme.mediumFont(size: 15))
253
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
265
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
254
         trailing.translatesAutoresizingMaskIntoConstraints = false
266
         trailing.translatesAutoresizingMaskIntoConstraints = false
255
 
267
 
@@ -278,9 +290,7 @@ private final class SettingsActionRow: SettingsRowBase {
278
         super.init(isLast: isLast)
290
         super.init(isLast: isLast)
279
 
291
 
280
         let badge = SettingsIconBadge(symbolName: symbolName)
292
         let badge = SettingsIconBadge(symbolName: symbolName)
281
-        let titleLabel = NSTextField(labelWithString: title)
282
-        titleLabel.font = AppTheme.mediumFont(size: 15)
283
-        titleLabel.textColor = AppTheme.textPrimary
293
+        let titleLabel = NSTextField.themeLabel(title, style: .primary, font: AppTheme.mediumFont(size: 15))
284
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
294
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
285
 
295
 
286
         addSubview(badge)
296
         addSubview(badge)

+ 8 - 4
smart_printer/SidebarView.swift

@@ -7,7 +7,8 @@ enum SidebarDestination: Int, CaseIterable {
7
     case settings
7
     case settings
8
 }
8
 }
9
 
9
 
10
-final class SidebarView: NSView {
10
+final class SidebarView: NSView, AppearanceRefreshable {
11
+    private var appNameLabel: NSTextField!
11
     var onDestinationSelected: ((SidebarDestination) -> Void)?
12
     var onDestinationSelected: ((SidebarDestination) -> Void)?
12
 
13
 
13
     private var navItems: [SidebarNavItem] = []
14
     private var navItems: [SidebarNavItem] = []
@@ -29,6 +30,11 @@ final class SidebarView: NSView {
29
         }
30
         }
30
     }
31
     }
31
 
32
 
33
+    func refreshAppearance() {
34
+        layer?.backgroundColor = AppTheme.sidebarBackground.cgColor
35
+        appNameLabel?.refreshThemeLabelColor()
36
+    }
37
+
32
     private func setup() {
38
     private func setup() {
33
         let logoContainer = NSView()
39
         let logoContainer = NSView()
34
         logoContainer.translatesAutoresizingMaskIntoConstraints = false
40
         logoContainer.translatesAutoresizingMaskIntoConstraints = false
@@ -41,9 +47,7 @@ final class SidebarView: NSView {
41
         }
47
         }
42
         logoIcon.contentTintColor = AppTheme.purple
48
         logoIcon.contentTintColor = AppTheme.purple
43
 
49
 
44
-        let appNameLabel = NSTextField(labelWithString: "Smart Printer")
45
-        appNameLabel.font = AppTheme.semiboldFont(size: 15)
46
-        appNameLabel.textColor = AppTheme.textPrimary
50
+        appNameLabel = NSTextField.themeLabel("Smart Printer", style: .primary, font: AppTheme.semiboldFont(size: 15))
47
         appNameLabel.alignment = .center
51
         appNameLabel.alignment = .center
48
         appNameLabel.translatesAutoresizingMaskIntoConstraints = false
52
         appNameLabel.translatesAutoresizingMaskIntoConstraints = false
49
 
53
 

+ 34 - 17
smart_printer/UIComponents.swift

@@ -28,15 +28,21 @@ final class GradientCardView: NSView {
28
 
28
 
29
 // MARK: - Wave Pattern
29
 // MARK: - Wave Pattern
30
 
30
 
31
-final class WavePatternView: NSView {
31
+final class WavePatternView: NSView, AppearanceRefreshable {
32
+    var fixedDotColor: NSColor?
33
+
32
     override var isOpaque: Bool { false }
34
     override var isOpaque: Bool { false }
33
 
35
 
36
+    func refreshAppearance() {
37
+        guard fixedDotColor == nil else { return }
38
+        needsDisplay = true
39
+    }
40
+
34
     override func draw(_ dirtyRect: NSRect) {
41
     override func draw(_ dirtyRect: NSRect) {
35
         super.draw(dirtyRect)
42
         super.draw(dirtyRect)
36
         guard let context = NSGraphicsContext.current?.cgContext else { return }
43
         guard let context = NSGraphicsContext.current?.cgContext else { return }
37
 
44
 
38
-        let dotColor = NSColor(calibratedWhite: 0.82, alpha: 0.55)
39
-        context.setFillColor(dotColor.cgColor)
45
+        context.setFillColor((fixedDotColor ?? AppTheme.waveDotColor).cgColor)
40
 
46
 
41
         let spacing: CGFloat = 14
47
         let spacing: CGFloat = 14
42
         let dotSize: CGFloat = 3
48
         let dotSize: CGFloat = 3
@@ -57,7 +63,7 @@ final class WavePatternView: NSView {
57
 
63
 
58
 // MARK: - Sidebar Nav Item
64
 // MARK: - Sidebar Nav Item
59
 
65
 
60
-final class SidebarNavItem: NSControl {
66
+final class SidebarNavItem: NSControl, AppearanceRefreshable {
61
     enum Style {
67
     enum Style {
62
         case normal
68
         case normal
63
         case active
69
         case active
@@ -86,6 +92,7 @@ final class SidebarNavItem: NSControl {
86
 
92
 
87
         titleLabel.stringValue = title
93
         titleLabel.stringValue = title
88
         titleLabel.font = AppTheme.mediumFont(size: 14)
94
         titleLabel.font = AppTheme.mediumFont(size: 14)
95
+        titleLabel.themeLabelStyle = .primary
89
         titleLabel.textColor = AppTheme.textPrimary
96
         titleLabel.textColor = AppTheme.textPrimary
90
 
97
 
91
         if let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: title) {
98
         if let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: title) {
@@ -127,6 +134,10 @@ final class SidebarNavItem: NSControl {
127
     @available(*, unavailable)
134
     @available(*, unavailable)
128
     required init?(coder: NSCoder) { nil }
135
     required init?(coder: NSCoder) { nil }
129
 
136
 
137
+    func refreshAppearance() {
138
+        applyStyle(isSelected ? .active : .normal)
139
+    }
140
+
130
     private func applyStyle(_ style: Style) {
141
     private func applyStyle(_ style: Style) {
131
         container.wantsLayer = true
142
         container.wantsLayer = true
132
         container.layer?.cornerRadius = AppTheme.cornerRadius
143
         container.layer?.cornerRadius = AppTheme.cornerRadius
@@ -296,13 +307,14 @@ final class QuickStartCardView: NSView {
296
 
307
 
297
         let subtitleLabel = NSTextField(labelWithString: data.subtitle)
308
         let subtitleLabel = NSTextField(labelWithString: data.subtitle)
298
         subtitleLabel.font = AppTheme.regularFont(size: 13)
309
         subtitleLabel.font = AppTheme.regularFont(size: 13)
299
-        subtitleLabel.textColor = AppTheme.textSecondary
310
+        subtitleLabel.textColor = AppTheme.quickStartCardSubtitle
300
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
311
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
301
 
312
 
302
         let button = PillButton(title: data.buttonTitle, color: data.accentColor)
313
         let button = PillButton(title: data.buttonTitle, color: data.accentColor)
303
         button.translatesAutoresizingMaskIntoConstraints = false
314
         button.translatesAutoresizingMaskIntoConstraints = false
304
 
315
 
305
         let wavePattern = WavePatternView()
316
         let wavePattern = WavePatternView()
317
+        wavePattern.fixedDotColor = AppTheme.quickStartWaveDot
306
         wavePattern.translatesAutoresizingMaskIntoConstraints = false
318
         wavePattern.translatesAutoresizingMaskIntoConstraints = false
307
 
319
 
308
         addSubview(gradient)
320
         addSubview(gradient)
@@ -365,39 +377,34 @@ struct FeatureCardData {
365
     let iconKind: FeatureIconKind
377
     let iconKind: FeatureIconKind
366
 }
378
 }
367
 
379
 
368
-final class FeatureCardView: NSView {
380
+final class FeatureCardView: NSView, AppearanceRefreshable {
369
     private let iconView: FeatureIconView
381
     private let iconView: FeatureIconView
382
+    private let titleLabel: NSTextField
383
+    private let subtitleLabel: NSTextField
384
+    private let arrowButton: NSButton
370
     private var iconWidthConstraint: NSLayoutConstraint!
385
     private var iconWidthConstraint: NSLayoutConstraint!
371
     private var iconHeightConstraint: NSLayoutConstraint!
386
     private var iconHeightConstraint: NSLayoutConstraint!
372
 
387
 
373
     init(data: FeatureCardData) {
388
     init(data: FeatureCardData) {
374
         iconView = FeatureIconView(kind: data.iconKind)
389
         iconView = FeatureIconView(kind: data.iconKind)
390
+        titleLabel = NSTextField.themeLabel(data.title, style: .primary, font: AppTheme.semiboldFont(size: 14))
391
+        subtitleLabel = NSTextField.themeLabel(data.subtitle, style: .secondary, font: AppTheme.regularFont(size: 11))
392
+        arrowButton = NSButton()
375
         super.init(frame: .zero)
393
         super.init(frame: .zero)
376
         translatesAutoresizingMaskIntoConstraints = false
394
         translatesAutoresizingMaskIntoConstraints = false
377
         setContentHuggingPriority(.defaultLow, for: .horizontal)
395
         setContentHuggingPriority(.defaultLow, for: .horizontal)
378
         setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
396
         setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
379
         wantsLayer = true
397
         wantsLayer = true
380
-        layer?.backgroundColor = AppTheme.cardBackground.cgColor
381
         layer?.cornerRadius = AppTheme.featureCardCornerRadius
398
         layer?.cornerRadius = AppTheme.featureCardCornerRadius
382
         applyCardShadow()
399
         applyCardShadow()
383
 
400
 
384
-        let titleLabel = NSTextField(labelWithString: data.title)
385
-        titleLabel.font = AppTheme.semiboldFont(size: 14)
386
-        titleLabel.textColor = AppTheme.textPrimary
387
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
401
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
388
-
389
-        let subtitleLabel = NSTextField(labelWithString: data.subtitle)
390
-        subtitleLabel.font = AppTheme.regularFont(size: 11)
391
-        subtitleLabel.textColor = AppTheme.textSecondary
392
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
402
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
393
 
403
 
394
-        let arrowButton = NSButton()
395
         arrowButton.isBordered = false
404
         arrowButton.isBordered = false
396
         arrowButton.wantsLayer = true
405
         arrowButton.wantsLayer = true
397
-        arrowButton.layer?.backgroundColor = NSColor.white.cgColor
398
         arrowButton.layer?.cornerRadius = 13
406
         arrowButton.layer?.cornerRadius = 13
399
         arrowButton.layer?.borderWidth = 1
407
         arrowButton.layer?.borderWidth = 1
400
-        arrowButton.layer?.borderColor = NSColor(calibratedWhite: 0.88, alpha: 1).cgColor
401
         arrowButton.translatesAutoresizingMaskIntoConstraints = false
408
         arrowButton.translatesAutoresizingMaskIntoConstraints = false
402
         if let arrow = NSImage(systemSymbolName: "chevron.right", accessibilityDescription: "Open") {
409
         if let arrow = NSImage(systemSymbolName: "chevron.right", accessibilityDescription: "Open") {
403
             let config = NSImage.SymbolConfiguration(pointSize: 10, weight: .semibold)
410
             let config = NSImage.SymbolConfiguration(pointSize: 10, weight: .semibold)
@@ -434,11 +441,21 @@ final class FeatureCardView: NSView {
434
         iconHeightConstraint = iconView.heightAnchor.constraint(equalToConstant: AppTheme.featureIconMax)
441
         iconHeightConstraint = iconView.heightAnchor.constraint(equalToConstant: AppTheme.featureIconMax)
435
         iconWidthConstraint.isActive = true
442
         iconWidthConstraint.isActive = true
436
         iconHeightConstraint.isActive = true
443
         iconHeightConstraint.isActive = true
444
+        refreshAppearance()
437
     }
445
     }
438
 
446
 
439
     @available(*, unavailable)
447
     @available(*, unavailable)
440
     required init?(coder: NSCoder) { nil }
448
     required init?(coder: NSCoder) { nil }
441
 
449
 
450
+    func refreshAppearance() {
451
+        layer?.backgroundColor = AppTheme.cardBackground.cgColor
452
+        titleLabel.refreshThemeLabelColor()
453
+        subtitleLabel.refreshThemeLabelColor()
454
+        arrowButton.layer?.backgroundColor = AppTheme.elevatedBackground.cgColor
455
+        arrowButton.layer?.borderColor = AppTheme.border.cgColor
456
+        arrowButton.contentTintColor = AppTheme.textSecondary
457
+    }
458
+
442
     override func layout() {
459
     override func layout() {
443
         super.layout()
460
         super.layout()
444
         let size = AppTheme.featureIconSize(forCardWidth: bounds.width)
461
         let size = AppTheme.featureIconSize(forCardWidth: bounds.width)

+ 23 - 7
smart_printer/ViewController.swift

@@ -38,6 +38,12 @@ class ViewController: NSViewController {
38
             name: .showSettings,
38
             name: .showSettings,
39
             object: nil
39
             object: nil
40
         )
40
         )
41
+        NotificationCenter.default.addObserver(
42
+            self,
43
+            selector: #selector(appearanceDidChange),
44
+            name: .appearanceDidChange,
45
+            object: nil
46
+        )
41
     }
47
     }
42
 
48
 
43
     deinit {
49
     deinit {
@@ -48,6 +54,19 @@ class ViewController: NSViewController {
48
         navigateToSettings()
54
         navigateToSettings()
49
     }
55
     }
50
 
56
 
57
+    @objc private func appearanceDidChange() {
58
+        refreshAppearance()
59
+    }
60
+
61
+    private func refreshAppearance() {
62
+        view.layer?.backgroundColor = AppTheme.background.cgColor
63
+        mainContentView?.layer?.backgroundColor = AppTheme.background.cgColor
64
+        view.window?.backgroundColor = AppTheme.background
65
+        sidebar?.refreshAppearance()
66
+        view.refreshAppearanceRecursively()
67
+        paywallOverlay?.refreshAppearance()
68
+    }
69
+
51
     private func setupLayout() {
70
     private func setupLayout() {
52
         sidebar = SidebarView()
71
         sidebar = SidebarView()
53
 
72
 
@@ -270,9 +289,7 @@ class ViewController: NSViewController {
270
         let header = NSView()
289
         let header = NSView()
271
         header.translatesAutoresizingMaskIntoConstraints = false
290
         header.translatesAutoresizingMaskIntoConstraints = false
272
 
291
 
273
-        let titleLabel = NSTextField(labelWithString: "Smart Printer")
274
-        titleLabel.font = AppTheme.semiboldFont(size: 18)
275
-        titleLabel.textColor = AppTheme.textPrimary
292
+        let titleLabel = NSTextField.themeLabel("Smart Printer", style: .primary, font: AppTheme.semiboldFont(size: 18))
276
         titleLabel.alignment = .center
293
         titleLabel.alignment = .center
277
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
294
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
278
 
295
 
@@ -375,6 +392,7 @@ class ViewController: NSViewController {
375
                     gridIcon.image = image.withSymbolConfiguration(config)
392
                     gridIcon.image = image.withSymbolConfiguration(config)
376
                 }
393
                 }
377
                 gridIcon.contentTintColor = AppTheme.textPrimary
394
                 gridIcon.contentTintColor = AppTheme.textPrimary
395
+                gridIcon.usesThemeTint = true
378
                 titleIcon = gridIcon
396
                 titleIcon = gridIcon
379
             case .none:
397
             case .none:
380
                 titleIcon = NSView()
398
                 titleIcon = NSView()
@@ -393,9 +411,7 @@ class ViewController: NSViewController {
393
             iconSpacing = 8
411
             iconSpacing = 8
394
         }
412
         }
395
 
413
 
396
-        let label = NSTextField(labelWithString: text)
397
-        label.font = .systemFont(ofSize: 20, weight: .bold)
398
-        label.textColor = AppTheme.textPrimary
414
+        let label = NSTextField.themeLabel(text, style: .primary, font: .systemFont(ofSize: 20, weight: .bold))
399
         label.translatesAutoresizingMaskIntoConstraints = false
415
         label.translatesAutoresizingMaskIntoConstraints = false
400
         container.addSubview(label)
416
         container.addSubview(label)
401
 
417
 
@@ -428,7 +444,7 @@ class ViewController: NSViewController {
428
                 subtitle: "Take a photo from gallery",
444
                 subtitle: "Take a photo from gallery",
429
                 buttonTitle: "Open Gallery",
445
                 buttonTitle: "Open Gallery",
430
                 accentColor: AppTheme.blue,
446
                 accentColor: AppTheme.blue,
431
-                gradientColors: [AppTheme.blueLight, NSColor(red: 0.82, green: 0.90, blue: 1.0, alpha: 1)],
447
+                gradientColors: [AppTheme.quickStartBlueLight, NSColor(red: 0.82, green: 0.90, blue: 1.0, alpha: 1)],
432
                 iconKind: .photos
448
                 iconKind: .photos
433
             ),
449
             ),
434
             QuickStartCardData(
450
             QuickStartCardData(