Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 66 additions & 35 deletions freewrite/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,22 @@ struct ContentView: View {
@State private var isHoveringHistoryText = false
@State private var isHoveringHistoryPath = false
@State private var isHoveringHistoryArrow = false
@State private var colorScheme: ColorScheme = .light // Add state for color scheme
@State private var isHoveringThemeToggle = false // Add state for theme toggle hover
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
let entryHeight: CGFloat = 40

@AppStorage("colorScheme") private var colorSchemeString: String = "auto"
@Environment(\.colorScheme) private var systemColorScheme
@State private var showingThemePopover = false
@State private var refreshID = UUID()
private var currentColorScheme: ColorScheme {
switch colorSchemeString {
case "light": return .light
case "dark": return .dark
default: return systemColorScheme
}
}

let availableFonts = NSFontManager.shared.availableFontFamilies
let standardFonts = ["Lato-Regular", "Arial", ".AppleSystemUIFont", "Times New Roman"]
let fontSizes: [CGFloat] = [16, 18, 20, 22, 24, 26]
Expand Down Expand Up @@ -149,13 +160,6 @@ struct ContentView: View {
Here's my journal entry:
"""

// Initialize with saved theme preference if available
init() {
// Load saved color scheme preference
let savedScheme = UserDefaults.standard.string(forKey: "colorScheme") ?? "light"
_colorScheme = State(initialValue: savedScheme == "dark" ? .dark : .light)
}

// Modify getDocumentsDirectory to use cached value
private func getDocumentsDirectory() -> URL {
return documentsDirectory
Expand Down Expand Up @@ -351,9 +355,9 @@ struct ContentView: View {

var timerColor: Color {
if timerIsRunning {
return isHoveringTimer ? (colorScheme == .light ? .black : .white) : .gray.opacity(0.8)
return isHoveringTimer ? (currentColorScheme == .light ? .black : .white) : .gray.opacity(0.8)
} else {
return isHoveringTimer ? (colorScheme == .light ? .black : .white) : (colorScheme == .light ? .gray : .gray.opacity(0.8))
return isHoveringTimer ? (currentColorScheme == .light ? .black : .white) : (currentColorScheme == .light ? .gray : .gray.opacity(0.8))
}
}

Expand All @@ -372,25 +376,24 @@ struct ContentView: View {
return fontSize / 2
}

// Add a color utility computed property
var popoverBackgroundColor: Color {
return colorScheme == .light ? Color(NSColor.controlBackgroundColor) : Color(NSColor.darkGray)
return currentColorScheme == .light ? Color(NSColor.controlBackgroundColor) : Color(NSColor.darkGray)
}

var popoverTextColor: Color {
return colorScheme == .light ? Color.primary : Color.white
return currentColorScheme == .light ? Color.primary : Color.white
}

var body: some View {
let buttonBackground = colorScheme == .light ? Color.white : Color.black
let buttonBackground = currentColorScheme == .light ? Color.white : Color.black
let navHeight: CGFloat = 68
let textColor = colorScheme == .light ? Color.gray : Color.gray.opacity(0.8)
let textHoverColor = colorScheme == .light ? Color.black : Color.white
let textColor = currentColorScheme == .light ? Color.gray : Color.gray.opacity(0.8)
let textHoverColor = currentColorScheme == .light ? Color.black : Color.white

HStack(spacing: 0) {
// Main content
ZStack {
Color(colorScheme == .light ? .white : .black)
Color(currentColorScheme == .light ? .white : .black)
.ignoresSafeArea()

TextEditor(text: Binding(
Expand All @@ -404,17 +407,16 @@ struct ContentView: View {
}
}
))
.background(Color(colorScheme == .light ? .white : .black))
.font(.custom(selectedFont, size: fontSize))
.foregroundColor(colorScheme == .light ? Color(red: 0.20, green: 0.20, blue: 0.20) : Color(red: 0.9, green: 0.9, blue: 0.9))
.foregroundColor(currentColorScheme == .light ? Color(red: 0.20, green: 0.20, blue: 0.20) : Color(red: 0.9, green: 0.9, blue: 0.9))
.scrollContentBackground(.hidden)
.scrollIndicators(.never)
.lineSpacing(lineHeight)
.frame(maxWidth: 650)
.id("\(selectedFont)-\(fontSize)-\(colorScheme)")
.id("\(selectedFont)-\(fontSize)-\(currentColorScheme)")
.padding(.bottom, bottomNavOpacity > 0 ? navHeight : 0)
.ignoresSafeArea()
.colorScheme(colorScheme)
.colorScheme(currentColorScheme)
.onAppear {
placeholderText = placeholderOptions.randomElement() ?? "\n\nBegin writing"
// Removed findSubview code which was causing errors
Expand All @@ -424,7 +426,7 @@ struct ContentView: View {
if text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
Text(placeholderText)
.font(.custom(selectedFont, size: fontSize))
.foregroundColor(colorScheme == .light ? .gray.opacity(0.5) : .gray.opacity(0.6))
.foregroundColor(currentColorScheme == .light ? .gray.opacity(0.5) : .gray.opacity(0.6))
// .padding(.top, 8)
// .padding(.leading, 8)
.allowsHitTesting(false)
Expand Down Expand Up @@ -630,7 +632,6 @@ struct ContentView: View {
.frame(width: 250)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(popoverBackgroundColor)
.cornerRadius(8)
.shadow(color: Color.black.opacity(0.1), radius: 4, y: 2)
} else if text.count < 350 {
Expand All @@ -640,7 +641,6 @@ struct ContentView: View {
.frame(width: 250)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(popoverBackgroundColor)
.cornerRadius(8)
.shadow(color: Color.black.opacity(0.1), radius: 4, y: 2)
} else {
Expand Down Expand Up @@ -672,7 +672,6 @@ struct ContentView: View {
.foregroundColor(popoverTextColor)
}
.frame(width: 120)
.background(popoverBackgroundColor)
.cornerRadius(8)
.shadow(color: Color.black.opacity(0.1), radius: 4, y: 2)
}
Expand Down Expand Up @@ -724,12 +723,13 @@ struct ContentView: View {

// Theme toggle button
Button(action: {
colorScheme = colorScheme == .light ? .dark : .light
// Save preference
UserDefaults.standard.set(colorScheme == .light ? "light" : "dark", forKey: "colorScheme")
showingThemePopover = true
}) {
Image(systemName: colorScheme == .light ? "moon.fill" : "sun.max.fill")
let theme = AppColorScheme(rawValue: colorSchemeString) ?? .auto
Image(systemName: theme.systemImage)
.frame(width: 18) // If you remove this, there will be flickering in the width of the bar.
.foregroundColor(isHoveringThemeToggle ? textHoverColor : textColor)
.animation(.easeInOut(duration: 0.3), value: colorSchemeString)
}
.buttonStyle(.plain)
.onHover { hovering in
Expand All @@ -741,6 +741,38 @@ struct ContentView: View {
NSCursor.pop()
}
}
.popover(isPresented: $showingThemePopover, attachmentAnchor: .point(UnitPoint(x: 0.5, y: 0)), arrowEdge: .top) {
VStack(spacing: 0) {
ForEach(["light", "dark", "auto"], id: \.self) { themeValue in
let theme = AppColorScheme(rawValue: themeValue) ?? .auto
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
colorSchemeString = themeValue
}
showingThemePopover = false
}) {
HStack {
Image(systemName: theme.systemImage)
.frame(width: 20)
Text(theme.displayName)
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.cornerRadius(4)
}
.buttonStyle(.plain)
.foregroundColor(popoverTextColor)

if themeValue != "auto" {
Divider()
}
}
}
.background(popoverBackgroundColor)
.cornerRadius(8)
.shadow(color: Color.black.opacity(0.1), radius: 4, y: 2)
}

Text("•")
.foregroundColor(.gray)
Expand Down Expand Up @@ -772,7 +804,6 @@ struct ContentView: View {
}
}
.padding()
.background(Color(colorScheme == .light ? .white : .black))
.opacity(bottomNavOpacity)
.onHover { hovering in
isHoveringBottomNav = hovering
Expand Down Expand Up @@ -861,8 +892,8 @@ struct ContentView: View {
Image(systemName: "arrow.down.circle")
.font(.system(size: 11))
.foregroundColor(hoveredExportId == entry.id ?
(colorScheme == .light ? .black : .white) :
(colorScheme == .light ? .gray : .gray.opacity(0.8)))
(currentColorScheme == .light ? .black : .white) :
(currentColorScheme == .light ? .gray : .gray.opacity(0.8)))
}
.buttonStyle(.plain)
.help("Export entry as PDF")
Expand Down Expand Up @@ -934,12 +965,12 @@ struct ContentView: View {
.scrollIndicators(.never)
}
.frame(width: 200)
.background(Color(colorScheme == .light ? .white : NSColor.black))
}
}
.frame(minWidth: 1100, minHeight: 600)
.animation(.easeInOut(duration: 0.2), value: showingSidebar)
.preferredColorScheme(colorScheme)
.animation(.easeInOut(duration: 0.3), value: currentColorScheme) // MARK : this sets the light/dark mode transition time
.preferredColorScheme(currentColorScheme)
.onAppear {
showingSidebar = false // Hide sidebar by default
loadExistingEntries()
Expand Down Expand Up @@ -1305,4 +1336,4 @@ extension NSView {

#Preview {
ContentView()
}
}
86 changes: 82 additions & 4 deletions freewrite/freewriteApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,85 @@
//

import SwiftUI
import AppKit

@main
struct freewriteApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@AppStorage("colorScheme") private var colorSchemeString: String = "light"
@AppStorage("colorScheme") private var colorSchemeString: String = "auto"
@StateObject private var appearanceManager = AppearanceManager()

init() {
// Register Lato font
if let fontURL = Bundle.main.url(forResource: "Lato-Regular", withExtension: "ttf") {
CTFontManagerRegisterFontsForURL(fontURL as CFURL, .process, nil)
}
}

var body: some Scene {
WindowGroup {
ContentView()
.toolbar(.hidden, for: .windowToolbar)
.preferredColorScheme(colorSchemeString == "dark" ? .dark : .light)
.preferredColorScheme(getPreferredColorScheme())
.environmentObject(appearanceManager)
}
.windowStyle(.hiddenTitleBar)
.defaultSize(width: 1100, height: 600)
.windowToolbarStyle(.unifiedCompact)
.windowResizability(.contentSize)
}

// Return desired appearance from user setting
private func getPreferredColorScheme() -> ColorScheme? {
switch colorSchemeString {
case "light":
return .light
case "dark":
return .dark
default:
return appearanceManager.colorScheme
}
}
}

@MainActor
class AppearanceManager: ObservableObject {
@Published var colorScheme: ColorScheme = .light
private var appearanceObserver: Any?

init() {
setupAppearanceObserver()
updateColorScheme()
}

private func setupAppearanceObserver() {
// Observe system appearance changes
appearanceObserver = DistributedNotificationCenter.default.addObserver(
forName: NSNotification.Name("AppleInterfaceThemeChangedNotification"),
object: nil,
queue: .main
) { [weak self] _ in
self?.updateColorScheme()
}
}

func updateColorScheme() {
let isDarkMode = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark"
colorScheme = isDarkMode ? .dark : .light

// Update app's appearance
DispatchQueue.main.async {
NSApp.appearance = isDarkMode ?
NSAppearance(named: .darkAqua) :
NSAppearance(named: .aqua)
}
}

deinit {
if let observer = appearanceObserver {
DistributedNotificationCenter.default.removeObserver(observer)
}
}
}

// Add AppDelegate to handle window configuration
Expand All @@ -45,4 +100,27 @@ class AppDelegate: NSObject, NSApplicationDelegate {
window.center()
}
}
}
}

// Names and icons of the three appearance options
enum AppColorScheme: String {
case light = "light"
case dark = "dark"
case auto = "auto"

var displayName: String {
switch self {
case .light: return "Light"
case .dark: return "Dark"
case .auto: return "Auto"
}
}

var systemImage: String {
switch self {
case .light: return "sun.max.fill"
case .dark: return "moon.fill"
case .auto: return "sun.dust.fill"
}
}
}