Skip to content
Merged
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
4 changes: 2 additions & 2 deletions App/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.6.0</string>
<string>0.7.0</string>
<key>CFBundleVersion</key>
<string>14</string>
<string>15</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>NSHighResolutionCapable</key>
Expand Down
1,126 changes: 694 additions & 432 deletions App/Resources/reader/app.js

Large diffs are not rendered by default.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added App/Resources/reader/fonts/KaTeX_Main-Bold.woff2
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 2 additions & 0 deletions App/Resources/reader/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<link rel="stylesheet" href="styles/themes.css" />
<link rel="stylesheet" href="styles/reader.css" />
<link rel="stylesheet" href="styles/hljs.css" />
<!-- F4: KaTeX 样式(构建时从 katex 包剥离 woff/ttf 拷贝到 dist/styles/katex.min.css)。 -->
<link rel="stylesheet" href="styles/katex.min.css" />
</head>
<body>
<div id="app">
Expand Down
1 change: 1 addition & 0 deletions App/Resources/reader/styles/katex.min.css

Large diffs are not rendered by default.

131 changes: 128 additions & 3 deletions App/Resources/reader/styles/reader.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
box-sizing: border-box;
}

:root {
--font-scale: 1;
--content-max-width-px: 672px;
}

html,
body {
margin: 0;
Expand All @@ -10,7 +15,8 @@ body {
color: var(--fg);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
font-size: 16px;
/* F1:屏幕字号随 --font-scale 缩放;rem 的 h1-h3 自动跟动。打印用 @media print 固定。 */
font-size: calc(16px * var(--font-scale, 1));
line-height: 1.65;
}

Expand Down Expand Up @@ -135,7 +141,7 @@ body {
}

.markdown-body > * {
max-width: 42rem;
max-width: var(--content-max-width-px, 42rem);
margin-left: auto;
margin-right: auto;
}
Expand Down Expand Up @@ -264,7 +270,7 @@ body {
border: none;
border-top: 1px solid var(--border);
margin: 2em auto;
max-width: 42rem;
max-width: var(--content-max-width-px, 42rem);
}

.markdown-body input[type="checkbox"] {
Expand Down Expand Up @@ -301,6 +307,100 @@ body {
border-color: #8a4040;
}

/* F4:KaTeX 块级公式横向溢出可滚,避免宽公式撑破栏宽。 */
.markdown-body .katex-display {
overflow-x: auto;
overflow-y: hidden;
padding: 4px 0;
}

/* F5:文内查找条(纯 JS,fixed top-right 胶囊)。 */
.find-bar {
position: fixed;
top: 8px;
right: 14px;
z-index: 10;
display: flex;
align-items: center;
gap: 6px;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: 8px;
padding: 4px 8px;
box-shadow: 0 2px 8px var(--shadow);
}
.find-bar.hidden {
display: none;
}
.find-bar input {
border: 1px solid var(--border);
background: var(--bg);
color: var(--fg);
border-radius: 6px;
padding: 2px 6px;
font-size: 13px;
width: 200px;
outline: none;
}
.find-bar input:focus {
border-color: var(--link);
}
.find-bar button {
border: 1px solid var(--border);
background: var(--bg);
color: var(--fg);
border-radius: 6px;
padding: 2px 8px;
font-size: 12px;
cursor: pointer;
}
.find-bar button:hover {
border-color: var(--fg-muted);
}
.find-case {
font-size: 12px;
color: var(--fg-muted);
display: flex;
align-items: center;
gap: 3px;
user-select: none;
}
mark.mdeye-find {
background: #ffe89a;
color: inherit;
border-radius: 2px;
}
mark.mdeye-find.current {
background: #ff9a3c;
}
[data-theme="dark"] mark.mdeye-find {
background: rgba(255, 200, 80, 0.4);
}
[data-theme="dark"] mark.mdeye-find.current {
background: rgba(255, 140, 40, 0.7);
}

/* F6:阅读器轻提示浮层(fixed bottom-center 胶囊,3s 自动消失)。 */
.mdeye-toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translateX(-50%);
background: var(--bg-elevated);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 14px;
font-size: 13px;
line-height: 1.45;
box-shadow: 0 4px 16px var(--shadow);
z-index: 20;
max-width: 80vw;
}
.mdeye-toast.warn {
border-color: #d39b00;
}

/* PDF export uses a dedicated WKWebView and WebKit's print pagination.
Paper size and 16 mm margins are configured by NSPrintInfo, not duplicated here. */
@media print {
Expand Down Expand Up @@ -338,6 +438,8 @@ body {
body {
background: #ffffff !important;
color: var(--fg) !important;
/* F1:打印固定字号,屏幕缩放不污染 PDF。 */
font-size: 16px !important;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
Expand All @@ -354,6 +456,29 @@ body {
margin-right: 0;
}

/* F1:栏宽变量在打印时同样收起铺满纸面。 */
.markdown-body hr {
max-width: none !important;
}

/* F4:块级公式横向溢出在分页时安全滚动,不再保留屏显的外边距。 */
.markdown-body .katex-display {
overflow: visible !important;
break-inside: avoid;
}

/* F5/F6:阅读器浮层不进 PDF。 */
.find-bar,
.mdeye-toast {
display: none !important;
}

/* F5:查找高亮在打印中退化为普通文本样式。 */
mark.mdeye-find {
background: transparent !important;
color: inherit !important;
}

a {
color: inherit !important;
text-decoration: none !important;
Expand Down
23 changes: 22 additions & 1 deletion App/Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,29 @@ final class AppDelegate: NSObject, NSApplicationDelegate {

// Some Launch Services paths only show up slightly after launch.
DispatchQueue.main.async { [weak self] in
self?.flushPendingPaths()
guard let self else { return }
self.flushPendingPaths()
// F2: 仍无外部传入 path → 尝试恢复上次打开的文件(单文件语义,
// 复用 openFile 入口,不开新窗/标签)。失效路径会被清空,不再重试。
if self.pendingPaths.isEmpty, self.windowController != nil {
self.restoreLastOpenedPath()
}
}
}

private func restoreLastOpenedPath() {
guard let last = Preferences.shared.lastOpenedPath,
!last.isEmpty,
FileManager.default.fileExists(atPath: last) else {
// 失效路径清空,避免下次启动再尝试并触发错误对话框。
if Preferences.shared.lastOpenedPath != nil {
Preferences.shared.lastOpenedPath = nil
NSLog("mdeye: lastOpenedPath cleared (missing)")
}
return
}
NSLog("mdeye: restoring lastOpenedPath → %@", last)
windowController?.openFile(path: last)
}

func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
Expand Down
29 changes: 29 additions & 0 deletions App/Sources/FileService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,40 @@ enum FileService {
if let s = String(data: data, encoding: .utf8) {
return (s, "utf-8")
}
// GB18030(GBK 超集):常见 Windows 中文 .md。.gb18030 便捷常量只在 Apple
// Foundation 提供,corelibs-foundation(CI 的 swift-corelibs)无此成员;
// 用 CFStringConvert 跨平台拿到 NSStringEncoding。
let gb18030 = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(
CFStringEncoding(CFStringEncodings.GB_18030_2000.rawValue)
))
if let s = String(data: data, encoding: gb18030) {
return (s, "gb18030")
}
if let s = String(data: data, encoding: .utf16) {
return (s, "utf-16")
}
// Fallback: lossy ISO Latin-1 style
let s = String(decoding: data, as: UTF8.self)
return (s, "utf-8-lossy")
}

/// 富文本标记命中检测(纯函数,便于 CI 自检)。命中即提示用户在 TextEdit 里用
/// 「格式→制作纯文本」清理,避免富文本保存破坏 Markdown 源。阈值保守:纯
/// ASCII / UTF-8 / GB18030 文本的非预期控制符正常为 0,不会误报。
static func looksLikeRichText(_ text: String) -> Bool {
if text.hasPrefix("{\\rtf1") { return true }
if text.range(of: "\\\\object\\\\objhtml", options: .regularExpression) != nil { return true }
if text.range(of: "\\\\fonttbl", options: .regularExpression) != nil { return true }
// 前 2KB 内非预期控制符密集出现(\x00-\x08 \x0B \x0C \x0E-\x1F;放行 \t \n \r)。
let head = text.prefix(2048)
var ctrl = 0
for c in head {
guard let s = c.asciiValue else { continue }
if s < 9 || (s > 10 && s < 13) || (s > 13 && s < 32) {
ctrl += 1
if ctrl >= 4 { return true }
}
}
return false
}
}
19 changes: 19 additions & 0 deletions App/Sources/MainWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ final class MainWindowController: NSWindowController, NSWindowDelegate {
viewMenu.addItem(withTitle: "Theme: Dark", action: #selector(setThemeDark(_:)), keyEquivalent: "2")
viewMenu.addItem(withTitle: "Theme: Sepia", action: #selector(setThemeSepia(_:)), keyEquivalent: "3")
viewMenu.addItem(withTitle: "Theme: Green", action: #selector(setThemeGreen(_:)), keyEquivalent: "4")
viewMenu.addItem(NSMenuItem.separator())
// F1:字号缩放(⌘+/⌘-/⌘0)与栏宽调整(⌥+/⌥-)。
viewMenu.addItem(withTitle: "Zoom In (Text)", action: #selector(zoomTextIn(_:)), keyEquivalent: "+")
viewMenu.addItem(withTitle: "Zoom Out (Text)", action: #selector(zoomTextOut(_:)), keyEquivalent: "-")
viewMenu.addItem(withTitle: "Reset Text Zoom", action: #selector(zoomTextReset(_:)), keyEquivalent: "0")
viewMenu.addItem(NSMenuItem.separator())
let widen = NSMenuItem(title: "Widen Column", action: #selector(widenColumn(_:)), keyEquivalent: "+")
widen.keyEquivalentModifierMask = [.option]
viewMenu.addItem(widen)
let narrow = NSMenuItem(title: "Narrow Column", action: #selector(narrowColumn(_:)), keyEquivalent: "-")
narrow.keyEquivalentModifierMask = [.option]
viewMenu.addItem(narrow)

let windowMenuItem = NSMenuItem()
mainMenu.addItem(windowMenuItem)
Expand Down Expand Up @@ -132,6 +144,13 @@ final class MainWindowController: NSWindowController, NSWindowDelegate {
@objc private func setThemeSepia(_ sender: Any?) { contentController.setTheme("sepia") }
@objc private func setThemeGreen(_ sender: Any?) { contentController.setTheme("green") }

// F1:屏幕缩放(字号 / 栏宽)。打印不沿用——CSS @media print 固定字号与 max-width。
@objc private func zoomTextIn(_ sender: Any?) { contentController.adjustFontSizeScale(by: 0.1) }
@objc private func zoomTextOut(_ sender: Any?) { contentController.adjustFontSizeScale(by: -0.1) }
@objc private func zoomTextReset(_ sender: Any?) { contentController.setFontSizeScale(1.0) }
@objc private func widenColumn(_ sender: Any?) { contentController.adjustContentMaxWidth(by: 32) }
@objc private func narrowColumn(_ sender: Any?) { contentController.adjustContentMaxWidth(by: -32) }

@objc private func setAsDefaultApp(_ sender: Any?) {
let result = DefaultAppService.setAsDefaultMarkdownViewer()
let alert = NSAlert()
Expand Down
27 changes: 27 additions & 0 deletions App/Sources/Preferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ final class Preferences {
static let theme = "theme"
static let lastOpenedPath = "lastOpenedPath"
static let outlineOpen = "outlineOpen"
static let fontSizeScale = "fontSizeScale"
static let contentMaxWidth = "contentMaxWidth"
}

var theme: String {
Expand All @@ -28,6 +30,24 @@ final class Preferences {
set { defaults.set(newValue, forKey: Key.outlineOpen) }
}

/// 字号缩放系数,默认 1.0,范围 0.85–2.0。屏幕阅读用;打印不沿用(@media print 固定)。
var fontSizeScale: Double {
get {
let v = defaults.object(forKey: Key.fontSizeScale) as? Double ?? 1.0
return min(max(v, 0.85), 2.0)
}
set { defaults.set(min(max(newValue, 0.85), 2.0), forKey: Key.fontSizeScale) }
}

/// 正文内容最大宽度(px),默认 672(= 42rem @ 16px),范围 600–1100。
var contentMaxWidth: Int {
get {
let v = defaults.object(forKey: Key.contentMaxWidth) as? Int ?? 672
return min(max(v, 600), 1100)
}
set { defaults.set(min(max(newValue, 600), 1100), forKey: Key.contentMaxWidth) }
}

func set(key: String, value: Any?) {
switch key {
case "theme":
Expand All @@ -36,6 +56,13 @@ final class Preferences {
if let b = value as? Bool { outlineOpen = b }
case "lastOpenedPath":
lastOpenedPath = value as? String
case "fontSizeScale":
// JS JSON 可能把整数化的 Double 传成 Int,统一转 Double。
if let n = value as? Double { fontSizeScale = n }
else if let n = value as? Int { fontSizeScale = Double(n) }
case "contentMaxWidth":
if let n = value as? Int { contentMaxWidth = n }
else if let n = value as? Double { contentMaxWidth = Int(n) }
default:
break
}
Expand Down
Loading
Loading