From 788155fcef85abc0ae0e89c11fbdc11b5b74dc88 Mon Sep 17 00:00:00 2001 From: hzl <1803573449@qq.com> Date: Tue, 4 Aug 2026 19:33:53 +0800 Subject: [PATCH] feat: add adaptive appearance settings --- README.md | 8 +-- README.zh-CN.md | 4 +- Resources/en.lproj/Localizable.strings | 5 ++ Resources/zh-Hans.lproj/Localizable.strings | 5 ++ Sources/Model/AppearanceStore.swift | 40 ++++++++++++++ Sources/Theme/Colors.swift | 14 +++++ Sources/Views/Settings/BrandHeader.swift | 10 ++-- Sources/Views/Settings/ChartStylePicker.swift | 8 +-- Sources/Views/Settings/CostStylePicker.swift | 6 +-- Sources/Views/Settings/SegmentedControl.swift | 19 ++++--- Sources/Views/Settings/SettingsFooter.swift | 12 ++--- Sources/Views/Settings/SettingsRow.swift | 10 ++-- Sources/Views/Settings/SettingsToggle.swift | 6 +-- Sources/Views/Settings/StyleTile.swift | 6 +-- Sources/Views/SettingsView.swift | 54 ++++++++++++------- Sources/Views/SettingsWindowController.swift | 14 +++-- 16 files changed, 157 insertions(+), 64 deletions(-) create mode 100644 Sources/Model/AppearanceStore.swift diff --git a/README.md b/README.md index a5382fdb..bc43b64a 100644 --- a/README.md +++ b/README.md @@ -184,8 +184,8 @@ Settings is a custom `NSWindow`, not the system Settings scene. The app still runs as an accessory app with no Dock icon and no menu bar. - **General:** Launch at Login, 5m/15m/30m refresh interval, app language, - Always show usage, Low Power Mode, configurable limit alerts, and Sparkle - update controls. + light/dark/system appearance, Always show usage, Low Power Mode, + configurable limit alerts, and Sparkle update controls. - **Display:** used/remaining percentages, Usage and Cost visualization styles, target display, and island width on non-notched screens. - **Providers:** Claude/Codex visibility and status, token-counting mode, and a @@ -193,8 +193,8 @@ runs as an accessory app with no Dock icon and no menu bar. Preferences are stored in `UserDefaults` under `MacIsland.*` keys (Sparkle manages its own `SU*` update keys, and Launch at Login uses -`SMAppService.mainApp`). Refresh, display, and provider changes apply live; -changing the app language offers to restart CodexIsland. +`SMAppService.mainApp`). Appearance, refresh, display, and provider changes +apply live; changing the app language offers to restart CodexIsland. ## Build from source diff --git a/README.zh-CN.md b/README.zh-CN.md index 9513e634..c1a25088 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -91,6 +91,7 @@ Claude: | 设置 | 存储 | UserDefaults key | 值 | | --- | --- | --- | --- | +| 外观 | `AppearanceStore` | `MacIsland.appearance` | `system`, `light`, `dark`,默认 `dark` | | 图表样式 | `StylePref` | `MacIsland.chartStyle` | `ring`, `bar`, `stepped`, `numeric`, `spark` | | 成本样式 | `CostStylePref` | `MacIsland.costStyle` | `dollar`, `multi`, `tokens`, `spark` | | Token 统计 | `TokenCountModeStore` | `MacIsland.tokenCountMode` | `all`, `billable` | @@ -100,7 +101,8 @@ Claude: | Codex 可见 | `ProviderVisibilityStore` | `MacIsland.codexVisible` | Boolean,默认 `true` | | 登录启动 | `LaunchAtLoginStore` | 由 `SMAppService.mainApp` 管理 | 系统登录项状态 | -刷新间隔会立即生效。`UsageStore` 会重置当前计时器,并用新的间隔重新安排下一次拉取。 +外观和刷新间隔都会立即生效;选择“跟随系统”后,设置窗口会随 macOS +浅色/深色外观自动切换。`UsageStore` 会重置当前计时器,并用新的间隔重新安排下一次拉取。 ## 从源码构建 diff --git a/Resources/en.lproj/Localizable.strings b/Resources/en.lproj/Localizable.strings index 7408446c..fb2f4c73 100644 --- a/Resources/en.lproj/Localizable.strings +++ b/Resources/en.lproj/Localizable.strings @@ -101,6 +101,11 @@ "Ring" = "Ring"; "scanning local logs…" = "scanning local logs…"; "Settings" = "Settings"; +"Appearance" = "Appearance"; +"Choose a light or dark skin, or follow macOS." = "Choose a light or dark skin, or follow macOS."; +"System" = "System"; +"Light" = "Light"; +"Dark" = "Dark"; "Show on" = "Show on"; "Sparkline" = "Sparkline"; "Spacing" = "Spacing"; diff --git a/Resources/zh-Hans.lproj/Localizable.strings b/Resources/zh-Hans.lproj/Localizable.strings index 54c83716..c593ffa6 100644 --- a/Resources/zh-Hans.lproj/Localizable.strings +++ b/Resources/zh-Hans.lproj/Localizable.strings @@ -101,6 +101,11 @@ "Ring" = "环形"; "scanning local logs…" = "正在扫描本地日志…"; "Settings" = "设置"; +"Appearance" = "外观"; +"Choose a light or dark skin, or follow macOS." = "选择浅色或深色皮肤,或跟随 macOS。"; +"System" = "跟随系统"; +"Light" = "浅色"; +"Dark" = "深色"; "Show on" = "显示在"; "Sparkline" = "折线"; "Spacing" = "间距"; diff --git a/Sources/Model/AppearanceStore.swift b/Sources/Model/AppearanceStore.swift new file mode 100644 index 00000000..f954ace5 --- /dev/null +++ b/Sources/Model/AppearanceStore.swift @@ -0,0 +1,40 @@ +import SwiftUI + +enum AppAppearance: String, CaseIterable, Hashable { + case system + case light + case dark + + var label: String { + switch self { + case .system: "System" + case .light: "Light" + case .dark: "Dark" + } + } + + var colorScheme: ColorScheme? { + switch self { + case .system: nil + case .light: .light + case .dark: .dark + } + } +} + +@MainActor +final class AppearanceStore: ObservableObject { + static let shared = AppearanceStore() + static let key = "MacIsland.appearance" + + @Published var appearance: AppAppearance { + didSet { + UserDefaults.standard.set(appearance.rawValue, forKey: Self.key) + } + } + + private init() { + let raw = UserDefaults.standard.string(forKey: Self.key) ?? "" + appearance = AppAppearance(rawValue: raw) ?? .dark + } +} diff --git a/Sources/Theme/Colors.swift b/Sources/Theme/Colors.swift index 85321f5b..b52e490e 100644 --- a/Sources/Theme/Colors.swift +++ b/Sources/Theme/Colors.swift @@ -1,7 +1,13 @@ +import AppKit import SwiftUI /// Locked color tokens for CodexIsland. enum IslandColor { + static let settingsBackground = Color( + light: NSColor(calibratedWhite: 0.965, alpha: 1), + dark: NSColor(calibratedRed: 0.020, green: 0.020, blue: 0.027, alpha: 1) + ) + /// #0047AB — loading sweep, glow halo. static let cobalt = Color(red: 0/255, green: 71/255, blue: 171/255) @@ -23,3 +29,11 @@ enum IslandColor { /// as "stop, you're cooked" without going full red-alert pure. static let alertRed = Color(red: 229/255, green: 72/255, blue: 77/255) } + +private extension Color { + init(light: NSColor, dark: NSColor) { + self.init(nsColor: NSColor(name: nil) { appearance in + appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua ? dark : light + }) + } +} diff --git a/Sources/Views/Settings/BrandHeader.swift b/Sources/Views/Settings/BrandHeader.swift index d7a98a0e..c3dd6215 100644 --- a/Sources/Views/Settings/BrandHeader.swift +++ b/Sources/Views/Settings/BrandHeader.swift @@ -24,21 +24,21 @@ struct BrandHeader: View { Text("CodexIsland") .font(Typography.brand) .tracking(-0.15) - .foregroundStyle(.white.opacity(0.92)) + .foregroundStyle(Color.primary.opacity(0.92)) Text(L10n.tr("Your AI usage limits, living in your notch.")) .font(Typography.label) - .foregroundStyle(.white.opacity(0.55)) + .foregroundStyle(Color.primary.opacity(0.55)) } Spacer(minLength: 8) Text("v\(version)") .font(Typography.bodyNumber) - .foregroundStyle(.white.opacity(0.34)) + .foregroundStyle(Color.primary.opacity(0.34)) .padding(.horizontal, 9) .padding(.vertical, 4) .background( - Capsule().fill(.white.opacity(0.04)) + Capsule().fill(Color.primary.opacity(0.04)) ) } .padding(.horizontal, 24) @@ -55,7 +55,7 @@ struct BrandHeader: View { .interpolation(.high) .aspectRatio(contentMode: .fit) .frame(width: 26, height: 26) - .foregroundStyle(.white.opacity(0.92)) + .foregroundStyle(Color.primary.opacity(0.92)) .shadow(color: IslandColor.cobalt.opacity(0.35), radius: 6) } else { // Fallback if the resource is missing in the bundle: a plain diff --git a/Sources/Views/Settings/ChartStylePicker.swift b/Sources/Views/Settings/ChartStylePicker.swift index fb4cc890..ddf4120e 100644 --- a/Sources/Views/Settings/ChartStylePicker.swift +++ b/Sources/Views/Settings/ChartStylePicker.swift @@ -34,7 +34,7 @@ struct ChartStylePicker: View { case .ring: ZStack { Circle() - .stroke(.white.opacity(0.10), lineWidth: 3) + .stroke(Color.primary.opacity(0.10), lineWidth: 3) Circle() .trim(from: 0, to: 0.35) .stroke(claude, style: StrokeStyle(lineWidth: 3, lineCap: .round)) @@ -43,7 +43,7 @@ struct ChartStylePicker: View { .frame(width: 26, height: 26) case .bar: ZStack(alignment: .leading) { - Capsule().fill(.white.opacity(0.10)) + Capsule().fill(Color.primary.opacity(0.10)) Capsule().fill(claude) .frame(width: 28 * 0.35) } @@ -52,7 +52,7 @@ struct ChartStylePicker: View { HStack(spacing: 1.5) { ForEach(0..<8) { i in RoundedRectangle(cornerRadius: 0.75) - .fill(i < 3 ? claude : .white.opacity(0.10)) + .fill(i < 3 ? claude : Color.primary.opacity(0.10)) .frame(width: 2, height: 12) } } @@ -64,7 +64,7 @@ struct ChartStylePicker: View { .foregroundStyle(claude) Text("%") .font(Typography.micro) - .foregroundStyle(.white.opacity(0.5)) + .foregroundStyle(Color.primary.opacity(0.5)) } case .spark: SparkPath() diff --git a/Sources/Views/Settings/CostStylePicker.swift b/Sources/Views/Settings/CostStylePicker.swift index 45e45a12..28df5566 100644 --- a/Sources/Views/Settings/CostStylePicker.swift +++ b/Sources/Views/Settings/CostStylePicker.swift @@ -34,14 +34,14 @@ struct CostStylePicker: View { HStack(alignment: .firstTextBaseline, spacing: 1) { Text("$") .font(Typography.micro) - .foregroundStyle(.white.opacity(0.5)) + .foregroundStyle(Color.primary.opacity(0.5)) Text("87") .font(Typography.previewNumber) .foregroundStyle(claude) } case .multi: HStack(alignment: .bottom, spacing: 4) { - Capsule().fill(.white.opacity(0.20)) + Capsule().fill(Color.primary.opacity(0.20)) .frame(width: 8, height: 6) Capsule().fill(claude) .frame(width: 8, height: 18) @@ -54,7 +54,7 @@ struct CostStylePicker: View { .foregroundStyle(claude) Text("M") .font(Typography.micro) - .foregroundStyle(.white.opacity(0.5)) + .foregroundStyle(Color.primary.opacity(0.5)) } case .spark: CostSparkPath() diff --git a/Sources/Views/Settings/SegmentedControl.swift b/Sources/Views/Settings/SegmentedControl.swift index f2f4e2bd..8e7ba0bd 100644 --- a/Sources/Views/Settings/SegmentedControl.swift +++ b/Sources/Views/Settings/SegmentedControl.swift @@ -21,16 +21,19 @@ struct SegmentedControl: View { Text(itemLabel) .font(Typography.bodyNumber) .foregroundStyle(isOn - ? Color.white.opacity(0.95) - : .white.opacity(0.55)) + ? Color.primary.opacity(0.95) + : Color.primary.opacity(0.55)) .padding(.horizontal, 10) .padding(.vertical, 5) .background { RoundedRectangle(cornerRadius: 5) - .fill(isOn ? .white.opacity(0.10) : .clear) + .fill(isOn ? Color.primary.opacity(0.10) : Color.clear) .overlay { RoundedRectangle(cornerRadius: 5) - .strokeBorder(.white.opacity(isOn ? 0.08 : 0), lineWidth: 0.5) + .strokeBorder( + Color.primary.opacity(isOn ? 0.08 : 0), + lineWidth: 0.5 + ) } } } @@ -45,7 +48,7 @@ struct SegmentedControl: View { .padding(2) .background { RoundedRectangle(cornerRadius: 7) - .fill(.white.opacity(0.04)) + .fill(Color.primary.opacity(0.04)) } } } @@ -62,15 +65,15 @@ struct PillButton: View { Button(action: action) { Text(L10n.tr(label)) .font(Typography.button) - .foregroundStyle(.white.opacity(0.9)) + .foregroundStyle(Color.primary.opacity(0.9)) .padding(.horizontal, 12) .padding(.vertical, 5) .background { RoundedRectangle(cornerRadius: 6) - .fill(.white.opacity(0.10)) + .fill(Color.primary.opacity(0.10)) .overlay { RoundedRectangle(cornerRadius: 6) - .strokeBorder(.white.opacity(0.08), lineWidth: 0.5) + .strokeBorder(Color.primary.opacity(0.08), lineWidth: 0.5) } } } diff --git a/Sources/Views/Settings/SettingsFooter.swift b/Sources/Views/Settings/SettingsFooter.swift index cab372e8..467ce70b 100644 --- a/Sources/Views/Settings/SettingsFooter.swift +++ b/Sources/Views/Settings/SettingsFooter.swift @@ -22,15 +22,15 @@ struct SettingsFooter: View { } label: { Text(L10n.tr("Quit")) .font(Typography.label) - .foregroundStyle(.white.opacity(quitHovered ? 0.92 : 0.55)) + .foregroundStyle(Color.primary.opacity(quitHovered ? 0.92 : 0.55)) .padding(.horizontal, 11) .padding(.vertical, 5) .background { RoundedRectangle(cornerRadius: 6) - .fill(.white.opacity(quitHovered ? 0.06 : 0.03)) + .fill(Color.primary.opacity(quitHovered ? 0.06 : 0.03)) .overlay { RoundedRectangle(cornerRadius: 6) - .strokeBorder(.white.opacity(0.07), lineWidth: 0.5) + .strokeBorder(Color.primary.opacity(0.07), lineWidth: 0.5) } } } @@ -62,14 +62,14 @@ private struct DottedLink: View { HStack(spacing: 4) { Text(L10n.tr(title)) .font(Typography.label) - .foregroundStyle(.white.opacity(hovered ? 0.92 : 0.55)) + .foregroundStyle(Color.primary.opacity(hovered ? 0.92 : 0.55)) Text("↗") .font(Typography.micro) - .foregroundStyle(.white.opacity(hovered ? 0.6 : 0.3)) + .foregroundStyle(Color.primary.opacity(hovered ? 0.6 : 0.3)) } .overlay(alignment: .bottom) { Rectangle() - .fill(.white.opacity(hovered ? 0.32 : 0.18)) + .fill(Color.primary.opacity(hovered ? 0.32 : 0.18)) .frame(height: 0.5) .offset(y: 1) .mask( diff --git a/Sources/Views/Settings/SettingsRow.swift b/Sources/Views/Settings/SettingsRow.swift index 3c169409..4ab0abb0 100644 --- a/Sources/Views/Settings/SettingsRow.swift +++ b/Sources/Views/Settings/SettingsRow.swift @@ -42,17 +42,17 @@ struct SettingsRow: View { Text(L10n.tr(title)) .font(Typography.rowTitle) .tracking(-0.07) - .foregroundStyle(.white.opacity(0.92)) + .foregroundStyle(Color.primary.opacity(0.92)) if let chip { Text(chip) .font(Typography.chip) .tracking(0.8) - .foregroundStyle(.white.opacity(0.6)) + .foregroundStyle(Color.primary.opacity(0.6)) .padding(.horizontal, 5) .padding(.vertical, 2) .background( RoundedRectangle(cornerRadius: 3) - .fill(.white.opacity(0.06)) + .fill(Color.primary.opacity(0.06)) ) .accessibilityLabel(L10n.tr("Plan: %@", chip)) } @@ -60,7 +60,7 @@ struct SettingsRow: View { if let subtitle { Text(L10n.tr(subtitle)) .font(Typography.label) - .foregroundStyle(.white.opacity(0.55)) + .foregroundStyle(Color.primary.opacity(0.55)) } } .accessibilityElement(children: .combine) @@ -73,7 +73,7 @@ struct SettingsRow: View { .padding(.vertical, 11) .background { RoundedRectangle(cornerRadius: 8) - .fill(.white.opacity(hovered ? 0.030 : 0)) + .fill(Color.primary.opacity(hovered ? 0.030 : 0)) } .contentShape(Rectangle()) .onHover { hovered = $0 } diff --git a/Sources/Views/Settings/SettingsToggle.swift b/Sources/Views/Settings/SettingsToggle.swift index ee4b4d6e..2bc7ddd8 100644 --- a/Sources/Views/Settings/SettingsToggle.swift +++ b/Sources/Views/Settings/SettingsToggle.swift @@ -16,16 +16,16 @@ struct SettingsToggle: View { Button(action: action) { ZStack(alignment: isOn ? .trailing : .leading) { Capsule() - .strokeBorder(.white.opacity(hovered ? 0.20 : 0.13), lineWidth: 1) + .strokeBorder(Color.primary.opacity(hovered ? 0.20 : 0.13), lineWidth: 1) .background { Capsule().fill(isOn ? IslandColor.cobalt.opacity(0.32) - : .white.opacity(0.07)) + : Color.primary.opacity(0.07)) } .frame(width: trackWidth, height: trackHeight) Circle() - .fill(isOn ? IslandColor.cobalt : Color.white.opacity(0.5)) + .fill(isOn ? IslandColor.cobalt : Color.primary.opacity(0.5)) .frame(width: dotSize, height: dotSize) .shadow( color: isOn ? IslandColor.cobalt.opacity(0.85) : .clear, diff --git a/Sources/Views/Settings/StyleTile.swift b/Sources/Views/Settings/StyleTile.swift index 3a48327f..5ab90623 100644 --- a/Sources/Views/Settings/StyleTile.swift +++ b/Sources/Views/Settings/StyleTile.swift @@ -19,8 +19,8 @@ struct StyleTile: View { Text(displayLabel) .font(Typography.micro) .foregroundStyle(isOn - ? Color(red: 0.58, green: 0.75, blue: 1.0) - : .white.opacity(0.55)) + ? Color.primary.opacity(0.90) + : Color.primary.opacity(0.55)) } .frame(maxWidth: .infinity) .padding(.top, 14) @@ -30,7 +30,7 @@ struct StyleTile: View { RoundedRectangle(cornerRadius: 9) .fill(isOn ? IslandColor.cobalt.opacity(0.14) - : .white.opacity(0.025)) + : Color.primary.opacity(0.025)) .overlay { RoundedRectangle(cornerRadius: 9) .strokeBorder(isOn diff --git a/Sources/Views/SettingsView.swift b/Sources/Views/SettingsView.swift index 9db95d40..f276bae5 100644 --- a/Sources/Views/SettingsView.swift +++ b/Sources/Views/SettingsView.swift @@ -20,6 +20,7 @@ struct SettingsView: View { @ObservedObject private var usageDisplay = UsageDisplayModeStore.shared @ObservedObject private var targetDisplay = IslandTargetDisplayStore.shared @ObservedObject private var appLanguage = AppLanguageStore.shared + @ObservedObject private var appearanceStore = AppearanceStore.shared @ObservedObject private var usage = UsageStore.shared @ObservedObject private var cost = CostStore.shared @ObservedObject private var updater = UpdaterController.shared @@ -67,8 +68,8 @@ struct SettingsView: View { SettingsFooter() } .frame(minWidth: 440, minHeight: 420) - .background(Color(red: 0.020, green: 0.020, blue: 0.027)) - .preferredColorScheme(.dark) + .background(IslandColor.settingsBackground) + .preferredColorScheme(appearanceStore.appearance.colorScheme) } // MARK: - Tabs @@ -105,16 +106,16 @@ struct SettingsView: View { Text(L10n.tr(tab.label)) .font(Typography.tabLabel) .foregroundStyle(isOn - ? .white.opacity(0.95) - : .white.opacity(0.50)) + ? Color.primary.opacity(0.95) + : Color.primary.opacity(0.50)) .padding(.horizontal, 12) .padding(.vertical, 6) .background { RoundedRectangle(cornerRadius: 6) - .fill(isOn ? .white.opacity(0.08) : .clear) + .fill(isOn ? Color.primary.opacity(0.08) : .clear) .overlay { RoundedRectangle(cornerRadius: 6) - .strokeBorder(.white.opacity(isOn ? 0.08 : 0), lineWidth: 0.5) + .strokeBorder(Color.primary.opacity(isOn ? 0.08 : 0), lineWidth: 0.5) } } } @@ -166,7 +167,7 @@ struct SettingsView: View { private var hairline: some View { LinearGradient( - colors: [.clear, .white.opacity(0.055), .white.opacity(0.055), .clear], + colors: [.clear, Color.primary.opacity(0.055), Color.primary.opacity(0.055), .clear], startPoint: .leading, endPoint: .trailing ) .frame(height: 1) @@ -179,12 +180,12 @@ struct SettingsView: View { .font(Typography.sectionLabel) .tracking(1.05) .textCase(.uppercase) - .foregroundStyle(.white.opacity(0.34)) + .foregroundStyle(Color.primary.opacity(0.34)) Spacer(minLength: 8) if let hint { Text(L10n.tr(hint)) .font(Typography.micro) - .foregroundStyle(.white.opacity(0.18)) + .foregroundStyle(Color.primary.opacity(0.18)) } } .padding(.horizontal, 10) @@ -214,6 +215,12 @@ struct SettingsView: View { ) { languagePicker } + SettingsRow( + title: "Appearance", + subtitle: "Choose a light or dark skin, or follow macOS." + ) { + appearanceSegmented + } SettingsRow( title: "Always show usage", subtitle: "Keep the percentage and time remaining visible without hovering." @@ -296,17 +303,17 @@ struct SettingsView: View { Button(action: action) { Text(L10n.tr(label)) .font(Typography.bodyNumber) - .foregroundStyle(.white.opacity(0.85)) + .foregroundStyle(Color.primary.opacity(0.85)) .lineLimit(1) .fixedSize() .padding(.horizontal, 9) .padding(.vertical, 4) .background { RoundedRectangle(cornerRadius: 5) - .fill(.white.opacity(0.06)) + .fill(Color.primary.opacity(0.06)) .overlay { RoundedRectangle(cornerRadius: 5) - .strokeBorder(.white.opacity(0.10), lineWidth: 0.5) + .strokeBorder(Color.primary.opacity(0.10), lineWidth: 0.5) } } } @@ -371,7 +378,7 @@ struct SettingsView: View { Text(L10n.tr(label)) .font(Typography.rowTitle) .tracking(-0.07) - .foregroundStyle(.white.opacity(0.92)) + .foregroundStyle(Color.primary.opacity(0.92)) Spacer(minLength: 8) thresholdStepper(value: value, range: range) } @@ -412,21 +419,21 @@ struct SettingsView: View { .textFieldStyle(.plain) .multilineTextAlignment(.center) .font(Typography.bodyNumber) - .foregroundStyle(.white.opacity(0.95)) + .foregroundStyle(Color.primary.opacity(0.95)) .monospacedDigit() .frame(width: 22, height: 18) .clipped() Text("%") .font(Typography.bodyNumber) - .foregroundStyle(.white.opacity(0.55)) + .foregroundStyle(Color.primary.opacity(0.55)) } .frame(width: 64, height: 28) .background { RoundedRectangle(cornerRadius: 7) - .fill(.white.opacity(0.05)) + .fill(Color.primary.opacity(0.05)) .overlay { RoundedRectangle(cornerRadius: 7) - .strokeBorder(.white.opacity(0.10), lineWidth: 0.5) + .strokeBorder(Color.primary.opacity(0.10), lineWidth: 0.5) } } } @@ -466,6 +473,15 @@ struct SettingsView: View { .accessibilityLabel(L10n.tr("Language")) } + private var appearanceSegmented: some View { + SegmentedControl( + items: AppAppearance.allCases, + selected: $appearanceStore.appearance, + label: \.label, + accessibilityPrefix: "Appearance" + ) + } + private var languageSelection: Binding { Binding( get: { appLanguage.language }, @@ -569,11 +585,11 @@ struct SettingsView: View { .font(Typography.sectionLabel) .tracking(1.05) .textCase(.uppercase) - .foregroundStyle(.white.opacity(0.34)) + .foregroundStyle(Color.primary.opacity(0.34)) Text(costSubtitle()) .font(Typography.label) - .foregroundStyle(.white.opacity(0.42)) + .foregroundStyle(Color.primary.opacity(0.42)) .lineLimit(1) .truncationMode(.tail) diff --git a/Sources/Views/SettingsWindowController.swift b/Sources/Views/SettingsWindowController.swift index 85038069..af4c05c1 100644 --- a/Sources/Views/SettingsWindowController.swift +++ b/Sources/Views/SettingsWindowController.swift @@ -1,4 +1,5 @@ import AppKit +import Combine import SwiftUI /// Hand-rolled NSWindow for Settings instead of the SwiftUI `Settings` scene. @@ -9,6 +10,7 @@ import SwiftUI @MainActor final class SettingsWindowController: NSWindowController, NSWindowDelegate { static let shared = SettingsWindowController() + private var appearanceSubscription: AnyCancellable? private init() { let hosting = NSHostingController(rootView: SettingsView()) @@ -23,9 +25,7 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate { window.titleVisibility = .hidden window.isMovableByWindowBackground = true window.isReleasedWhenClosed = false - window.backgroundColor = NSColor( - calibratedRed: 0.075, green: 0.077, blue: 0.090, alpha: 1 - ) + window.backgroundColor = .windowBackgroundColor window.minSize = NSSize(width: 440, height: 420) // Hide the dock-stow button (we have no dock icon) but keep zoom // alongside resize handles so the user controls size. @@ -33,6 +33,14 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate { window.center() super.init(window: window) window.delegate = self + appearanceSubscription = AppearanceStore.shared.$appearance + .sink { [weak window] appearance in + switch appearance { + case .system: window?.appearance = nil + case .light: window?.appearance = NSAppearance(named: .aqua) + case .dark: window?.appearance = NSAppearance(named: .darkAqua) + } + } } @available(*, unavailable)