Skip to content

Commit 96dfbc3

Browse files
committed
Add dark mode support and theme toggle
1 parent 3f57802 commit 96dfbc3

3 files changed

Lines changed: 90 additions & 30 deletions

File tree

Sources/Heard/MTApp.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,15 @@ struct HeardApp: App {
8989
var body: some Scene {
9090
MenuBarExtra {
9191
MenuBarView(model: appModel)
92+
.preferredColorScheme(appModel.settingsStore.settings.appearance.colorScheme)
9293
} label: {
9394
MenuBarIcon(model: appModel)
9495
}
9596
.menuBarExtraStyle(.window)
9697

9798
Window("Heard Settings", id: "settings") {
9899
SettingsView(model: appModel)
100+
.preferredColorScheme(appModel.settingsStore.settings.appearance.colorScheme)
99101
.onAppear { WindowActivationCoordinator.begin("settings") }
100102
.onDisappear { WindowActivationCoordinator.end("settings") }
101103
}
@@ -104,6 +106,7 @@ struct HeardApp: App {
104106

105107
Window("Name Speakers", id: "speaker-naming") {
106108
SpeakerNamingView(model: appModel)
109+
.preferredColorScheme(appModel.settingsStore.settings.appearance.colorScheme)
107110
.onAppear { WindowActivationCoordinator.begin("speaker-naming") }
108111
.onDisappear {
109112
WindowActivationCoordinator.end("speaker-naming")

Sources/HeardCore/CoreModels.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,22 @@ public enum AppPhase: String, Codable, CaseIterable {
1818
}
1919
}
2020

21+
public enum AppAppearance: String, Codable, CaseIterable, Identifiable {
22+
case system
23+
case light
24+
case dark
25+
26+
public var id: String { rawValue }
27+
28+
public var displayName: String {
29+
switch self {
30+
case .system: return "System"
31+
case .light: return "Light"
32+
case .dark: return "Dark"
33+
}
34+
}
35+
}
36+
2137
public enum PipelineStage: String, Codable, CaseIterable, Identifiable {
2238
case queued
2339
case preprocessing
@@ -336,6 +352,7 @@ public struct AppSettings: Codable, Equatable {
336352
/// over-split speakers in the Speakers tab, which is easier than recovering
337353
/// from a merged-embedding poisoning a profile.
338354
public var diarizationClusteringThreshold: Double
355+
public var appearance: AppAppearance
339356

340357
public static let `default` = AppSettings(
341358
userName: "",
@@ -360,7 +377,8 @@ public struct AppSettings: Codable, Equatable {
360377
enableTeamsDetection: true,
361378
enableZoomDetection: true,
362379
enableWebexDetection: true,
363-
diarizationClusteringThreshold: 0.5
380+
diarizationClusteringThreshold: 0.5,
381+
appearance: .system
364382
)
365383

366384
public init(
@@ -383,7 +401,8 @@ public struct AppSettings: Codable, Equatable {
383401
enableTeamsDetection: Bool = true,
384402
enableZoomDetection: Bool = true,
385403
enableWebexDetection: Bool = true,
386-
diarizationClusteringThreshold: Double = 0.5
404+
diarizationClusteringThreshold: Double = 0.5,
405+
appearance: AppAppearance = .system
387406
) {
388407
self.userName = userName
389408
self.launchAtLogin = launchAtLogin
@@ -405,6 +424,7 @@ public struct AppSettings: Codable, Equatable {
405424
self.enableZoomDetection = enableZoomDetection
406425
self.enableWebexDetection = enableWebexDetection
407426
self.diarizationClusteringThreshold = diarizationClusteringThreshold
427+
self.appearance = appearance
408428
}
409429
}
410430

Sources/HeardCore/Views.swift

Lines changed: 65 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,54 @@ private extension Color {
1414
blue: Double( n & 0xFF) / 255
1515
)
1616
}
17+
init(light: String, dark: String) {
18+
self.init(nsColor: NSColor(name: nil, dynamicProvider: { appearance in
19+
if appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua {
20+
return NSColor(Color(hex: dark))
21+
} else {
22+
return NSColor(Color(hex: light))
23+
}
24+
}))
25+
}
26+
}
27+
28+
// MARK: - AppAppearance Extension
29+
30+
public extension AppAppearance {
31+
var colorScheme: ColorScheme? {
32+
switch self {
33+
case .system: return nil
34+
case .light: return .light
35+
case .dark: return .dark
36+
}
37+
}
1738
}
1839

1940
// MARK: - Theme
2041

2142
enum HeardTheme {
2243
enum Paper {
23-
static let bg = Color(hex: "F5EFE4")
24-
static let surface = Color(hex: "FBF7EF")
25-
static let surfaceAlt = Color(hex: "EFE7D7")
26-
static let sidebar = Color(hex: "EBE2CE")
27-
static let border = Color(hex: "D9CFB9")
28-
static let borderSoft = Color(hex: "E5DCC8")
29-
static let ink = Color(hex: "1C2024")
30-
static let ink2 = Color(hex: "3A3F47")
31-
static let mute = Color(hex: "7B7264")
32-
static let muteSoft = Color(hex: "C9BBA5")
33-
static let accent = Color(hex: "3F5C8C")
34-
static let accentInk = Color(hex: "2F4570")
35-
static let accentSoft = Color(hex: "E5EAF3")
36-
static let good = Color(hex: "3D7A4F")
37-
static let goodSoft = Color(hex: "E1EEDF")
38-
static let warn = Color(hex: "A66A1F")
39-
static let warnSoft = Color(hex: "F4E6CE")
40-
static let bad = Color(hex: "A6452B")
41-
static let badSoft = Color(hex: "F2DCD2")
42-
static let recordingBg = Color(hex: "2E3338")
43-
static let recordingInk = Color(hex: "F5EFE4")
44+
static let bg = Color(light: "F5EFE4", dark: "1C2024")
45+
static let surface = Color(light: "FBF7EF", dark: "252A30")
46+
static let surfaceAlt = Color(light: "EFE7D7", dark: "2E3338")
47+
static let sidebar = Color(light: "EBE2CE", dark: "22272D")
48+
static let border = Color(light: "D9CFB9", dark: "4A515A")
49+
static let borderSoft = Color(light: "E5DCC8", dark: "3A3F47")
50+
static let ink = Color(light: "1C2024", dark: "F5EFE4")
51+
static let ink2 = Color(light: "3A3F47", dark: "D9CFB9")
52+
static let mute = Color(light: "7B7264", dark: "9A9184")
53+
static let muteSoft = Color(light: "C9BBA5", dark: "4A515A")
54+
static let accent = Color(light: "3F5C8C", dark: "658BC9")
55+
static let accentInk = Color(light: "2F4570", dark: "8BB2F2")
56+
static let accentSoft = Color(light: "E5EAF3", dark: "26334A")
57+
static let good = Color(light: "3D7A4F", dark: "53A66B")
58+
static let goodSoft = Color(light: "E1EEDF", dark: "243D2D")
59+
static let warn = Color(light: "A66A1F", dark: "D98A29")
60+
static let warnSoft = Color(light: "F4E6CE", dark: "4D351A")
61+
static let bad = Color(light: "A6452B", dark: "D65738")
62+
static let badSoft = Color(light: "F2DCD2", dark: "4A251C")
63+
static let recordingBg = Color(light: "2E3338", dark: "A6452B")
64+
static let recordingInk = Color(light: "F5EFE4", dark: "1C2024")
4465
}
4566

4667
static var accent: Color { Paper.accent }
@@ -387,7 +408,6 @@ public struct MenuBarView: View {
387408
}
388409
.frame(width: 268)
389410
.background(HeardTheme.Paper.bg)
390-
.preferredColorScheme(.light)
391411
.onChange(of: model.showNamingPrompt) { _, show in
392412
NSLog("Heard: MenuBarView observed showNamingPrompt=\(show)")
393413
if show {
@@ -778,7 +798,6 @@ public struct SettingsView: View {
778798
detailPane
779799
}
780800
.frame(minWidth: 580, minHeight: 440)
781-
.preferredColorScheme(.light)
782801
.sheet(item: $hotkeyTarget) { target in
783802
switch target {
784803
case .dictation:
@@ -790,7 +809,6 @@ public struct SettingsView: View {
790809
onCancel: { hotkeyTarget = nil },
791810
conflictingHotkey: model.settingsStore.settings.meetingNoteHotkey
792811
)
793-
.preferredColorScheme(.light)
794812
case .meetingNote:
795813
HotkeyRecorderView(
796814
onCommit: { combo in
@@ -800,7 +818,6 @@ public struct SettingsView: View {
800818
onCancel: { hotkeyTarget = nil },
801819
conflictingHotkey: model.settingsStore.settings.dictationHotkey
802820
)
803-
.preferredColorScheme(.light)
804821
}
805822
}
806823
}
@@ -914,6 +931,27 @@ public struct SettingsView: View {
914931
}
915932
}
916933

934+
sectionGroup("Appearance") {
935+
SettingsCard {
936+
CardRow(isLast: true) {
937+
HStack {
938+
Text("Theme")
939+
.font(.system(size: 12, weight: .medium))
940+
.foregroundStyle(HeardTheme.Paper.ink)
941+
Spacer()
942+
Picker("", selection: settingsBinding(\.appearance)) {
943+
ForEach(AppAppearance.allCases) { appearance in
944+
Text(appearance.displayName).tag(appearance)
945+
}
946+
}
947+
.labelsHidden()
948+
.controlSize(.small)
949+
.frame(maxWidth: 160)
950+
}
951+
}
952+
}
953+
}
954+
917955
sectionGroup("Behavior") {
918956
SettingsCard {
919957
ToggleRow(
@@ -1527,9 +1565,8 @@ public struct SettingsView: View {
15271565

15281566
Spacer().frame(height: 40)
15291567
}
1530-
.frame(maxWidth: .infinity)
1531-
}
1532-
.background(HeardTheme.Paper.bg)
1568+
.frame(minWidth: 500, minHeight: 500)
1569+
.background(HeardTheme.Paper.bg)
15331570
}
15341571
.frame(maxWidth: .infinity, maxHeight: .infinity)
15351572
.background(HeardTheme.Paper.bg)

0 commit comments

Comments
 (0)