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
10 changes: 8 additions & 2 deletions MDViewer/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
27 changes: 26 additions & 1 deletion MDViewer/MDViewerApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,31 @@ 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
ContentView(
document: file.document,
fileURL: file.fileURL,
appearanceMode: AppearanceMode(rawValue: appearanceMode) ?? .system,
zoomLevel: zoomLevel
zoomLevel: zoomLevel,
theme: selectTheme(),
)
}
.commands {
Expand Down Expand Up @@ -73,6 +90,14 @@ struct MDViewerApp: App {
}
}
}

Settings {
PreferencesView(
lightThemeID: $lightThemeID,
darkThemeID: $darkThemeID
)
}
.restorationBehavior(.disabled)
}

private func shortcut(for mode: AppearanceMode) -> KeyboardShortcut {
Expand Down
21 changes: 6 additions & 15 deletions MDViewer/MarkdownWebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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))
}
84 changes: 84 additions & 0 deletions MDViewer/PreferencesView.swift
Original file line number Diff line number Diff line change
@@ -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"))
}
22 changes: 1 addition & 21 deletions MDViewer/Resources/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root {
--color-bg: #ffffff;
--color-fg: #24292f;
--color-border: #d0d7de;
--color-code-bg: #f6f8fa;
--color-link: #0969da;
--color-blockquote-fg: #656d76;
--color-blockquote-border: #d0d7de;
--color-hr: #d8dee4;
}

@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0d1117;
--color-fg: #e6edf3;
--color-border: #30363d;
--color-code-bg: #161b22;
--color-link: #58a6ff;
--color-blockquote-fg: #8b949e;
--color-blockquote-border: #30363d;
--color-hr: #21262d;
}
{{THEME_CSS}}
}

* {
Expand Down
138 changes: 138 additions & 0 deletions MDViewer/Theme.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import SwiftUI

struct Theme: Identifiable {
let id: String
let name: String
let colors: ThemeColors
}

struct ThemeColors {
let bg: String
let fg: String
let border: String
let codeBg: String
let link: String
let blockquoteFg: String
let blockquoteBorder: String
let hr: String

func cssVariables() -> String {
return """
--color-bg: \(bg);
--color-fg: \(fg);
--color-border: \(border);
--color-code-bg: \(codeBg);
--color-link: \(link);
--color-blockquote-fg: \(blockquoteFg);
--color-blockquote-border: \(blockquoteBorder);
--color-hr: \(hr);
"""
}
}

extension Theme {
static let themes: [Theme] = [
Theme(
id: "github-light",
name: "GitHub Light",
colors: ThemeColors(
bg: "#ffffff",
fg: "#24292f",
border: "#d0d7de",
codeBg: "#f6f8fa",
link: "#0969da",
blockquoteFg: "#656d76",
blockquoteBorder: "#d0d7de",
hr: "#d8dee4"
)
),
Theme(
id: "solarized-light",
name: "Solarized Light",
colors: ThemeColors(
bg: "#fdf6e3",
fg: "#657b83",
border: "#93a1a1",
codeBg: "#eee8d5",
link: "#268bd2",
blockquoteFg: "#586e75",
blockquoteBorder: "#93a1a1",
hr: "#93a1a1"
)
),
Theme(
id: "github-dark",
name: "GitHub Dark",
colors: ThemeColors(
bg: "#0d1117",
fg: "#e6edf3",
border: "#30363d",
codeBg: "#161b22",
link: "#58a6ff",
blockquoteFg: "#8b949e",
blockquoteBorder: "#30363d",
hr: "#21262d"
)
),
Theme(
id: "dracula",
name: "Dracula",
colors: ThemeColors(
bg: "#282a36",
fg: "#f8f8f2",
border: "#44475a",
codeBg: "#44475a",
link: "#bd93f9",
blockquoteFg: "#6272a4",
blockquoteBorder: "#44475a",
hr: "#44475a"
)
),
Theme(
id: "solarized-dark",
name: "Solarized Dark",
colors: ThemeColors(
bg: "#002b36",
fg: "#839496",
border: "#073642",
codeBg: "#073642",
link: "#268bd2",
blockquoteFg: "#586e75",
blockquoteBorder: "#073642",
hr: "#073642"
)
),
Theme(
id: "monokai",
name: "Monokai",
colors: ThemeColors(
bg: "#272822",
fg: "#f8f8f2",
border: "#49483e",
codeBg: "#3e3d32",
link: "#66d9ef",
blockquoteFg: "#75715e",
blockquoteBorder: "#49483e",
hr: "#49483e"
)
),
Theme(
id: "nord",
name: "Nord",
colors: ThemeColors(
bg: "#2e3440",
fg: "#eceff4",
border: "#4c566a",
codeBg: "#3b4252",
link: "#88c0d0",
blockquoteFg: "#d8dee9",
blockquoteBorder: "#4c566a",
hr: "#4c566a"
)
)
]

static func theme(for id: String, in themes: [Theme]) -> Theme {
themes.first { $0.id == id } ?? themes[0]
}
}