From aa726b1a7326f6d207e801508ba5bf14ca9456d4 Mon Sep 17 00:00:00 2001 From: Asher Weintraub <11475126+asherweintraub@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:31:01 -0400 Subject: [PATCH] Add Theme Switcher --- MDViewer/ContentView.swift | 10 ++- MDViewer/MDViewerApp.swift | 27 +++++- MDViewer/MarkdownWebView.swift | 21 ++--- MDViewer/PreferencesView.swift | 84 +++++++++++++++++++ MDViewer/Resources/template.html | 22 +---- MDViewer/Theme.swift | 138 +++++++++++++++++++++++++++++++ 6 files changed, 263 insertions(+), 39 deletions(-) create mode 100644 MDViewer/PreferencesView.swift create mode 100644 MDViewer/Theme.swift diff --git a/MDViewer/ContentView.swift b/MDViewer/ContentView.swift index b08d2bf..d3b28b7 100644 --- a/MDViewer/ContentView.swift +++ b/MDViewer/ContentView.swift @@ -5,18 +5,24 @@ struct ContentView: View { let fileURL: URL? let appearanceMode: AppearanceMode let zoomLevel: Double + let theme: Theme @State private var text: String - init(document: MarkdownDocument, fileURL: URL?, appearanceMode: AppearanceMode, zoomLevel: Double) { + init(document: MarkdownDocument, fileURL: URL?, appearanceMode: AppearanceMode, zoomLevel: Double, theme: Theme) { self.document = document self.fileURL = fileURL self.appearanceMode = appearanceMode self.zoomLevel = zoomLevel self._text = State(initialValue: document.text) + self.theme = theme } var body: some View { - MarkdownWebView(text: text, appearanceMode: appearanceMode, zoomLevel: zoomLevel) + MarkdownWebView( + text: text, + zoomLevel: zoomLevel, + theme: theme + ) .onReceive(NotificationCenter.default.publisher(for: .reloadDocument)) { _ in reload() } diff --git a/MDViewer/MDViewerApp.swift b/MDViewer/MDViewerApp.swift index 63d1e8a..1bc71a2 100644 --- a/MDViewer/MDViewerApp.swift +++ b/MDViewer/MDViewerApp.swift @@ -22,6 +22,22 @@ enum AppearanceMode: String, CaseIterable { struct MDViewerApp: App { @AppStorage("appearanceMode") private var appearanceMode: String = AppearanceMode.system.rawValue @AppStorage("zoomLevel") private var zoomLevel: Double = 1.0 + @AppStorage("lightThemeID") private var lightThemeID: String = "github-light" + @AppStorage("darkThemeID") private var darkThemeID: String = "github-dark" + + private func selectTheme() -> Theme { + let lightTheme = Theme.theme(for: lightThemeID, in: Theme.themes) + let darkTheme = Theme.theme(for: darkThemeID, in: Theme.themes) + switch AppearanceMode(rawValue: appearanceMode) { + case .light: + return lightTheme + case .dark: + return darkTheme + default: + let isDark = NSApp.effectiveAppearance.name == .darkAqua + return isDark ? darkTheme : lightTheme + } + } var body: some Scene { DocumentGroup(viewing: MarkdownDocument.self) { file in @@ -29,7 +45,8 @@ struct MDViewerApp: App { document: file.document, fileURL: file.fileURL, appearanceMode: AppearanceMode(rawValue: appearanceMode) ?? .system, - zoomLevel: zoomLevel + zoomLevel: zoomLevel, + theme: selectTheme(), ) } .commands { @@ -73,6 +90,14 @@ struct MDViewerApp: App { } } } + + Settings { + PreferencesView( + lightThemeID: $lightThemeID, + darkThemeID: $darkThemeID + ) + } + .restorationBehavior(.disabled) } private func shortcut(for mode: AppearanceMode) -> KeyboardShortcut { diff --git a/MDViewer/MarkdownWebView.swift b/MDViewer/MarkdownWebView.swift index 69b7150..f993bef 100644 --- a/MDViewer/MarkdownWebView.swift +++ b/MDViewer/MarkdownWebView.swift @@ -3,38 +3,24 @@ import WebKit struct MarkdownWebView: NSViewRepresentable { let text: String - let appearanceMode: AppearanceMode let zoomLevel: Double + let theme: Theme func makeNSView(context: Context) -> WKWebView { let config = WKWebViewConfiguration() config.preferences.setValue(true, forKey: "developerExtrasEnabled") let webView = WKWebView(frame: .zero, configuration: config) - webView.setValue(false, forKey: "drawsBackground") - applyAppearance(to: webView) webView.pageZoom = zoomLevel loadContent(into: webView) return webView } func updateNSView(_ webView: WKWebView, context: Context) { - applyAppearance(to: webView) webView.pageZoom = zoomLevel loadContent(into: webView) } - private func applyAppearance(to webView: WKWebView) { - switch appearanceMode { - case .system: - webView.appearance = nil - case .light: - webView.appearance = NSAppearance(named: .aqua) - case .dark: - webView.appearance = NSAppearance(named: .darkAqua) - } - } - private func loadContent(into webView: WKWebView) { guard let templateURL = Bundle.main.url(forResource: "template", withExtension: "html"), let markedURL = Bundle.main.url(forResource: "marked.min", withExtension: "js"), @@ -48,9 +34,14 @@ struct MarkdownWebView: NSViewRepresentable { .replacingOccurrences(of: "$", with: "\\$") html = html + .replacingOccurrences(of: "{{THEME_CSS}}", with: theme.colors.cssVariables()) .replacingOccurrences(of: "{{MARKED_JS}}", with: markedJS) .replacingOccurrences(of: "{{MARKDOWN_CONTENT}}", with: escaped) webView.loadHTMLString(html, baseURL: templateURL.deletingLastPathComponent()) } } + +#Preview { + MarkdownWebView(text: "# This is a test\n1. Test\n1. Test\n1. Test", zoomLevel: 1.0, theme: Theme.theme(for: "github-light", in: Theme.themes)) +} diff --git a/MDViewer/PreferencesView.swift b/MDViewer/PreferencesView.swift new file mode 100644 index 0000000..71d4334 --- /dev/null +++ b/MDViewer/PreferencesView.swift @@ -0,0 +1,84 @@ +import SwiftUI + +struct PreferencesView: View { + @AppStorage("appearanceMode") private var appearanceMode: String = AppearanceMode.system.rawValue + @AppStorage("zoomLevel") private var zoomLevel: Double = 1.0 + @Binding var lightThemeID: String + @Binding var darkThemeID: String + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + Section { + Picker("Appearance", selection: $appearanceMode) { + ForEach(AppearanceMode.allCases, id: \.rawValue) { mode in + Text(mode.label).tag(mode.rawValue) + } + } + .pickerStyle(.segmented) + } + + Section { + Picker("Light Theme", selection: $lightThemeID) { + ForEach(Theme.themes) { theme in + Text(theme.name).tag(theme.id) + } + } + } + + Section { + Picker("Dark Theme", selection: $darkThemeID) { + ForEach(Theme.themes) { theme in + Text(theme.name).tag(theme.id) + } + } + } + + Section { + HStack(alignment: .center, spacing: 10) { + Text("Zoom") + Slider( + value: $zoomLevel, + in: 0.25...1.75, + step: 0.15 + ) + Text(String(format: "%.0f", zoomLevel * 100) + "%") + .font(.caption) + .foregroundColor(.secondary) + } + } + + Divider() + + HStack { + Spacer() + Button("Reset to Defaults") { + appearanceMode = AppearanceMode.system.rawValue + lightThemeID = "github-light" + darkThemeID = "github-dark" + zoomLevel = 1.0 + } + } + } + .onChange(of: appearanceMode) { _, newValue in + updateAppAppearance(newValue) + } + .padding(20) + .frame(width: 300) + } + + private func updateAppAppearance(_ mode: String) { + let appMode = AppearanceMode(rawValue: mode) ?? .system + switch appMode { + case .system: + NSApp.appearance = nil + case .light: + NSApp.appearance = NSAppearance(named: .aqua) + case .dark: + NSApp.appearance = NSAppearance(named: .darkAqua) + } + } +} + +#Preview { + PreferencesView(lightThemeID: .constant("github-light"), darkThemeID: .constant("github-dark")) +} diff --git a/MDViewer/Resources/template.html b/MDViewer/Resources/template.html index 854faca..ce58e5a 100644 --- a/MDViewer/Resources/template.html +++ b/MDViewer/Resources/template.html @@ -5,27 +5,7 @@