Skip to content

Commit b46b419

Browse files
committed
fix dark mode
1 parent 8836921 commit b46b419

File tree

2 files changed

+155
-36
lines changed

2 files changed

+155
-36
lines changed

freewrite/ContentView.swift

Lines changed: 69 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,22 @@ struct ContentView: View {
8282
@State private var isHoveringHistoryText = false
8383
@State private var isHoveringHistoryPath = false
8484
@State private var isHoveringHistoryArrow = false
85-
@State private var colorScheme: ColorScheme = .light // Add state for color scheme
8685
@State private var isHoveringThemeToggle = false // Add state for theme toggle hover
8786
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
8887
let entryHeight: CGFloat = 40
8988

89+
@AppStorage("colorScheme") private var colorSchemeString: String = "auto"
90+
@Environment(\.colorScheme) private var systemColorScheme
91+
@State private var showingThemePopover = false
92+
@State private var refreshID = UUID()
93+
private var currentColorScheme: ColorScheme {
94+
switch colorSchemeString {
95+
case "light": return .light
96+
case "dark": return .dark
97+
default: return systemColorScheme
98+
}
99+
}
100+
90101
let availableFonts = NSFontManager.shared.availableFontFamilies
91102
let standardFonts = ["Lato-Regular", "Arial", ".AppleSystemUIFont", "Times New Roman"]
92103
let fontSizes: [CGFloat] = [16, 18, 20, 22, 24, 26]
@@ -149,13 +160,6 @@ struct ContentView: View {
149160
Here's my journal entry:
150161
"""
151162

152-
// Initialize with saved theme preference if available
153-
init() {
154-
// Load saved color scheme preference
155-
let savedScheme = UserDefaults.standard.string(forKey: "colorScheme") ?? "light"
156-
_colorScheme = State(initialValue: savedScheme == "dark" ? .dark : .light)
157-
}
158-
159163
// Modify getDocumentsDirectory to use cached value
160164
private func getDocumentsDirectory() -> URL {
161165
return documentsDirectory
@@ -351,9 +355,9 @@ struct ContentView: View {
351355

352356
var timerColor: Color {
353357
if timerIsRunning {
354-
return isHoveringTimer ? (colorScheme == .light ? .black : .white) : .gray.opacity(0.8)
358+
return isHoveringTimer ? (currentColorScheme == .light ? .black : .white) : .gray.opacity(0.8)
355359
} else {
356-
return isHoveringTimer ? (colorScheme == .light ? .black : .white) : (colorScheme == .light ? .gray : .gray.opacity(0.8))
360+
return isHoveringTimer ? (currentColorScheme == .light ? .black : .white) : (currentColorScheme == .light ? .gray : .gray.opacity(0.8))
357361
}
358362
}
359363

@@ -372,25 +376,24 @@ struct ContentView: View {
372376
return fontSize / 2
373377
}
374378

375-
// Add a color utility computed property
376379
var popoverBackgroundColor: Color {
377-
return colorScheme == .light ? Color(NSColor.controlBackgroundColor) : Color(NSColor.darkGray)
380+
return currentColorScheme == .light ? Color(NSColor.controlBackgroundColor) : Color(NSColor.darkGray)
378381
}
379382

380383
var popoverTextColor: Color {
381-
return colorScheme == .light ? Color.primary : Color.white
384+
return currentColorScheme == .light ? Color.primary : Color.white
382385
}
383386

384387
var body: some View {
385-
let buttonBackground = colorScheme == .light ? Color.white : Color.black
388+
let buttonBackground = currentColorScheme == .light ? Color.white : Color.black
386389
let navHeight: CGFloat = 68
387-
let textColor = colorScheme == .light ? Color.gray : Color.gray.opacity(0.8)
388-
let textHoverColor = colorScheme == .light ? Color.black : Color.white
390+
let textColor = currentColorScheme == .light ? Color.gray : Color.gray.opacity(0.8)
391+
let textHoverColor = currentColorScheme == .light ? Color.black : Color.white
389392

390393
HStack(spacing: 0) {
391394
// Main content
392395
ZStack {
393-
Color(colorScheme == .light ? .white : .black)
396+
Color(currentColorScheme == .light ? .white : .black)
394397
.ignoresSafeArea()
395398

396399
TextEditor(text: Binding(
@@ -404,17 +407,17 @@ struct ContentView: View {
404407
}
405408
}
406409
))
407-
.background(Color(colorScheme == .light ? .white : .black))
410+
.background(Color(currentColorScheme == .light ? .white : .black))
408411
.font(.custom(selectedFont, size: fontSize))
409-
.foregroundColor(colorScheme == .light ? Color(red: 0.20, green: 0.20, blue: 0.20) : Color(red: 0.9, green: 0.9, blue: 0.9))
412+
.foregroundColor(currentColorScheme == .light ? Color(red: 0.20, green: 0.20, blue: 0.20) : Color(red: 0.9, green: 0.9, blue: 0.9))
410413
.scrollContentBackground(.hidden)
411414
.scrollIndicators(.never)
412415
.lineSpacing(lineHeight)
413416
.frame(maxWidth: 650)
414-
.id("\(selectedFont)-\(fontSize)-\(colorScheme)")
417+
.id("\(selectedFont)-\(fontSize)-\(currentColorScheme)")
415418
.padding(.bottom, bottomNavOpacity > 0 ? navHeight : 0)
416419
.ignoresSafeArea()
417-
.colorScheme(colorScheme)
420+
.colorScheme(currentColorScheme)
418421
.onAppear {
419422
placeholderText = placeholderOptions.randomElement() ?? "\n\nBegin writing"
420423
// Removed findSubview code which was causing errors
@@ -424,7 +427,7 @@ struct ContentView: View {
424427
if text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
425428
Text(placeholderText)
426429
.font(.custom(selectedFont, size: fontSize))
427-
.foregroundColor(colorScheme == .light ? .gray.opacity(0.5) : .gray.opacity(0.6))
430+
.foregroundColor(currentColorScheme == .light ? .gray.opacity(0.5) : .gray.opacity(0.6))
428431
// .padding(.top, 8)
429432
// .padding(.leading, 8)
430433
.allowsHitTesting(false)
@@ -724,12 +727,13 @@ struct ContentView: View {
724727

725728
// Theme toggle button
726729
Button(action: {
727-
colorScheme = colorScheme == .light ? .dark : .light
728-
// Save preference
729-
UserDefaults.standard.set(colorScheme == .light ? "light" : "dark", forKey: "colorScheme")
730+
showingThemePopover = true
730731
}) {
731-
Image(systemName: colorScheme == .light ? "moon.fill" : "sun.max.fill")
732+
let theme = AppColorScheme(rawValue: colorSchemeString) ?? .auto
733+
Image(systemName: theme.systemImage)
734+
.frame(width: 18) // If you remove this, there will be flickering in the width of the bar.
732735
.foregroundColor(isHoveringThemeToggle ? textHoverColor : textColor)
736+
.animation(.easeInOut(duration: 0.3), value: colorSchemeString)
733737
}
734738
.buttonStyle(.plain)
735739
.onHover { hovering in
@@ -741,6 +745,39 @@ struct ContentView: View {
741745
NSCursor.pop()
742746
}
743747
}
748+
.popover(isPresented: $showingThemePopover, attachmentAnchor: .point(UnitPoint(x: 0.5, y: 0)), arrowEdge: .top) {
749+
VStack(spacing: 0) {
750+
ForEach(["light", "dark", "auto"], id: \.self) { themeValue in
751+
let theme = AppColorScheme(rawValue: themeValue) ?? .auto
752+
Button(action: {
753+
withAnimation(.easeInOut(duration: 0.3)) {
754+
colorSchemeString = themeValue
755+
}
756+
showingThemePopover = false
757+
}) {
758+
HStack {
759+
Image(systemName: theme.systemImage)
760+
.frame(width: 20)
761+
Text(theme.displayName)
762+
.frame(maxWidth: .infinity, alignment: .leading)
763+
}
764+
.padding(.horizontal, 12)
765+
.padding(.vertical, 8)
766+
.background(colorSchemeString == themeValue ? Color.gray.opacity(0.2) : Color.clear)
767+
.cornerRadius(4)
768+
}
769+
.buttonStyle(.plain)
770+
.foregroundColor(popoverTextColor)
771+
772+
if themeValue != "auto" {
773+
Divider()
774+
}
775+
}
776+
}
777+
.background(popoverBackgroundColor)
778+
.cornerRadius(8)
779+
.shadow(color: Color.black.opacity(0.1), radius: 4, y: 2)
780+
}
744781

745782
Text("")
746783
.foregroundColor(.gray)
@@ -772,7 +809,7 @@ struct ContentView: View {
772809
}
773810
}
774811
.padding()
775-
.background(Color(colorScheme == .light ? .white : .black))
812+
.background(Color(currentColorScheme == .light ? .white : .black))
776813
.opacity(bottomNavOpacity)
777814
.onHover { hovering in
778815
isHoveringBottomNav = hovering
@@ -861,8 +898,8 @@ struct ContentView: View {
861898
Image(systemName: "arrow.down.circle")
862899
.font(.system(size: 11))
863900
.foregroundColor(hoveredExportId == entry.id ?
864-
(colorScheme == .light ? .black : .white) :
865-
(colorScheme == .light ? .gray : .gray.opacity(0.8)))
901+
(currentColorScheme == .light ? .black : .white) :
902+
(currentColorScheme == .light ? .gray : .gray.opacity(0.8)))
866903
}
867904
.buttonStyle(.plain)
868905
.help("Export entry as PDF")
@@ -934,12 +971,12 @@ struct ContentView: View {
934971
.scrollIndicators(.never)
935972
}
936973
.frame(width: 200)
937-
.background(Color(colorScheme == .light ? .white : NSColor.black))
974+
.background(Color(currentColorScheme == .light ? .white : NSColor.black))
938975
}
939976
}
940977
.frame(minWidth: 1100, minHeight: 600)
941978
.animation(.easeInOut(duration: 0.2), value: showingSidebar)
942-
.preferredColorScheme(colorScheme)
979+
.preferredColorScheme(currentColorScheme)
943980
.onAppear {
944981
showingSidebar = false // Hide sidebar by default
945982
loadExistingEntries()
@@ -1305,4 +1342,4 @@ extension NSView {
13051342

13061343
#Preview {
13071344
ContentView()
1308-
}
1345+
}

freewrite/freewriteApp.swift

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,89 @@
66
//
77

88
import SwiftUI
9+
import AppKit
910

1011
@main
1112
struct freewriteApp: App {
1213
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
13-
@AppStorage("colorScheme") private var colorSchemeString: String = "light"
14+
@AppStorage("colorScheme") private var colorSchemeString: String = "auto"
15+
@StateObject private var appearanceManager = AppearanceManager()
1416

1517
init() {
1618
// Register Lato font
1719
if let fontURL = Bundle.main.url(forResource: "Lato-Regular", withExtension: "ttf") {
1820
CTFontManagerRegisterFontsForURL(fontURL as CFURL, .process, nil)
1921
}
2022
}
21-
23+
2224
var body: some Scene {
2325
WindowGroup {
2426
ContentView()
2527
.toolbar(.hidden, for: .windowToolbar)
26-
.preferredColorScheme(colorSchemeString == "dark" ? .dark : .light)
28+
.preferredColorScheme(getPreferredColorScheme())
29+
.environmentObject(appearanceManager)
2730
}
2831
.windowStyle(.hiddenTitleBar)
2932
.defaultSize(width: 1100, height: 600)
3033
.windowToolbarStyle(.unifiedCompact)
3134
.windowResizability(.contentSize)
3235
}
36+
37+
// Return desired appearance from user setting
38+
private func getPreferredColorScheme() -> ColorScheme? {
39+
switch colorSchemeString {
40+
case "light":
41+
return .light
42+
case "dark":
43+
return .dark
44+
default:
45+
return appearanceManager.colorScheme
46+
}
47+
}
48+
}
49+
50+
// Dedicated class to manage appearance changes
51+
@MainActor
52+
class AppearanceManager: ObservableObject {
53+
@Published var colorScheme: ColorScheme = .light
54+
private var appearanceObserver: Any?
55+
56+
init() {
57+
setupAppearanceObserver()
58+
updateColorScheme()
59+
}
60+
61+
private func setupAppearanceObserver() {
62+
// Observe system appearance changes
63+
appearanceObserver = DistributedNotificationCenter.default.addObserver(
64+
forName: NSNotification.Name("AppleInterfaceThemeChangedNotification"),
65+
object: nil,
66+
queue: .main
67+
) { [weak self] _ in
68+
self?.updateColorScheme()
69+
}
70+
}
71+
72+
func updateColorScheme() {
73+
// Correctly detect dark mode
74+
let isDarkMode = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark"
75+
76+
// Correctly map system appearance to ColorScheme
77+
colorScheme = isDarkMode ? .dark : .light
78+
79+
// Update app's appearance
80+
DispatchQueue.main.async {
81+
NSApp.appearance = isDarkMode ?
82+
NSAppearance(named: .darkAqua) :
83+
NSAppearance(named: .aqua)
84+
}
85+
}
86+
87+
deinit {
88+
if let observer = appearanceObserver {
89+
DistributedNotificationCenter.default.removeObserver(observer)
90+
}
91+
}
3392
}
3493

3594
// Add AppDelegate to handle window configuration
@@ -45,4 +104,27 @@ class AppDelegate: NSObject, NSApplicationDelegate {
45104
window.center()
46105
}
47106
}
48-
}
107+
}
108+
109+
// Names and icons of the three appearance options
110+
enum AppColorScheme: String {
111+
case light = "light"
112+
case dark = "dark"
113+
case auto = "auto"
114+
115+
var displayName: String {
116+
switch self {
117+
case .light: return "Light"
118+
case .dark: return "Dark"
119+
case .auto: return "Auto"
120+
}
121+
}
122+
123+
var systemImage: String {
124+
switch self {
125+
case .light: return "sun.max.fill"
126+
case .dark: return "moon.fill"
127+
case .auto: return "sun.dust.fill"
128+
}
129+
}
130+
}

0 commit comments

Comments
 (0)