diff --git a/README.md b/README.md index b960efb..6fe94eb 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ ## 🤔 为什么做这个 -Windows 下一直没有好用的 Markdown 预览工具。想快速看一眼 `.md` 文件的内容,要么得打开 VS Code 这样的重量级编辑器,要么得用浏览器配合插件,体验都不够便捷。 +Vibe Coding 时代,AI 代码生成变得越来越普遍。项目中充斥着大量的 `.md` 文件——需求文档、架构设计、接口说明、PRD……频繁查阅这些文档成了日常刚需。但每次都要打开 VS Code 或浏览器,体验笨重且割裂。 + +Windows 下也一直没有好用的 Markdown 预览工具。想快速看一眼 `.md` 文件的内容,要么得打开 VS Code 这样的重量级编辑器,要么得用浏览器配合插件,体验都不够便捷。 **md++** 就是为了解决这个问题——一个轻量、纯粹的 Markdown 阅读器,双击文件即可预览,需要时也能快速编辑。**v0.3 新增的监听模式**,能像专业 diff 工具一样监听文件的外部变化、自动计算差异并可视化展示变更内容。 @@ -107,7 +109,7 @@ React 18 + TS ── 渲染管线 / UI / 状态管理 / 业务逻辑 | 场景 | 说明 | |------|------| | **Markdown 快速预览** | 双击 `.md` 文件即开即看,无需启动 IDE | -| **Typora 替代品** | 免费、开源、轻量的 Markdown 阅读器 | +| **Vibe Coding 搭档** | 查阅 AI 生成的需求文档、架构设计、PRD,轻量不拖沓 | | **Windows Markdown 查看器** | 原生 Windows 应用,非 Electron,内存占用低 | | **macOS Markdown 阅读器** | 原生 macOS 适配,支持 `.dmg` 安装 | | **技术文档阅读** | KaTeX 数学公式 + Mermaid 图表 + 代码高亮 | diff --git a/README_EN.md b/README_EN.md index d328ac4..703647a 100644 --- a/README_EN.md +++ b/README_EN.md @@ -17,6 +17,8 @@ **md++** is a fast, lightweight **Markdown viewer and reader** built with [Tauri v2](https://tauri.app) (Rust + React). Open `.md` files instantly on **Windows** or **macOS** — no heavy editors needed. +In the era of **Vibe Coding**, AI-generated code is everywhere — and so are `.md` files: requirements documents, architecture designs, API specs, PRDs. You need to check them constantly, but launching VS Code or a browser every time is overkill. **md++** fills that gap — a distraction-free Markdown viewer that stays out of your way. + **Highlights:** Full Markdown rendering (KaTeX math, Mermaid diagrams, shiki code highlighting), multi-tab browsing, TOC navigation, full-text search, PDF/HTML export, dark/light themes, file-watching auto-reload, Zen focus mode, and **file monitoring** — watch external file changes and visualize content diffs in real time. [📥 Download](https://github.com/Gitleonan/easy-md/releases) · [🐛 Report Issue](https://github.com/Gitleonan/easy-md/issues) · [💬 Discussions](https://github.com/Gitleonan/easy-md/discussions) @@ -96,7 +98,7 @@ React 18 + TS ── Render pipeline / UI / State management / Business logic | Scenario | Description | |----------|-------------| | **Quick Markdown preview** | Double-click `.md` files — no IDE required | -| **Typora alternative** | Free, open-source, lightweight Markdown reader | +| **Vibe Coding companion** | Browse AI-generated requirements, architecture docs, and PRDs on the fly | | **Windows Markdown viewer** | Native Windows app (not Electron); low memory footprint | | **macOS Markdown reader** | Native macOS support with `.dmg` installation | | **Technical docs** | KaTeX math + Mermaid diagrams + syntax-highlighted code | diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 48475a7..2620107 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -16,6 +16,7 @@ "fs:allow-exists", "opener:default", "opener:allow-open-url", - "opener:allow-open-path" + "opener:allow-open-path", + "core:webview:allow-print" ] } diff --git a/src-tauri/src/commands/opener.rs b/src-tauri/src/commands/opener.rs index b7a2c73..47270c1 100644 --- a/src-tauri/src/commands/opener.rs +++ b/src-tauri/src/commands/opener.rs @@ -5,6 +5,47 @@ use std::process::Command; #[cfg(target_os = "macos")] use std::io::Write; +/// 用系统默认程序打开任意文件(绕过 opener plugin 的路径 ACL) +/// 适用于打开 app data dir 下的临时导出文件等不受 opener plugin 信任的路径 +#[tauri::command] +pub async fn open_file_with_system(path: String) -> Result<(), String> { + if path.trim().is_empty() { + return Err("文件路径不能为空".to_string()); + } + let path = PathBuf::from(path); + tauri::async_runtime::spawn_blocking(move || open_file(&path)) + .await + .map_err(|e| format!("打开文件失败: {}", e))? +} + +#[cfg(target_os = "windows")] +fn open_file(path: &Path) -> Result<(), String> { + Command::new("cmd") + .args(["/C", "start", "", "/B"]) + .arg(path) + .spawn() + .map_err(|e| format!("启动关联程序失败: {}", e))?; + Ok(()) +} + +#[cfg(target_os = "macos")] +fn open_file(path: &Path) -> Result<(), String> { + Command::new("open") + .arg(path) + .spawn() + .map_err(|e| format!("启动关联程序失败: {}", e))?; + Ok(()) +} + +#[cfg(all(unix, not(target_os = "macos")))] +fn open_file(path: &Path) -> Result<(), String> { + Command::new("xdg-open") + .arg(path) + .spawn() + .map_err(|e| format!("启动关联程序失败: {}", e))?; + Ok(()) +} + #[tauri::command] pub async fn open_containing_folder(path: String) -> Result<(), String> { if path.trim().is_empty() { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 79b8627..abcfc0b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -49,6 +49,7 @@ pub fn run() { commands::files::delete_custom_theme, commands::files::get_app_data_dir, commands::opener::open_containing_folder, + commands::opener::open_file_with_system, commands::opener::check_file_association, commands::opener::register_file_association, commands::recent::list_recent, diff --git a/src/features/export/export.test.ts b/src/features/export/export.test.ts index 777b826..9e35480 100644 --- a/src/features/export/export.test.ts +++ b/src/features/export/export.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { save } from '@tauri-apps/plugin-dialog'; -import { writeFile } from '../../ipc/files'; +import { writeFile, getAppDataDir } from '../../ipc/files'; +import { openFileWithSystem } from '../../ipc/opener'; import { buildStandaloneHtml, exportDocument } from './export'; vi.mock('@tauri-apps/plugin-dialog', () => ({ @@ -9,16 +10,26 @@ vi.mock('@tauri-apps/plugin-dialog', () => ({ vi.mock('../../ipc/files', () => ({ writeFile: vi.fn().mockResolvedValue(undefined), + getAppDataDir: vi.fn().mockResolvedValue('/app/data'), +})); + +vi.mock('../../ipc/opener', () => ({ + openFileWithSystem: vi.fn().mockResolvedValue(undefined), })); describe('export', () => { const saveMock = vi.mocked(save); const writeFileMock = vi.mocked(writeFile); + const openFileMock = vi.mocked(openFileWithSystem); + const getAppDataDirMock = vi.mocked(getAppDataDir); beforeEach(() => { saveMock.mockReset(); writeFileMock.mockClear(); + openFileMock.mockClear(); + getAppDataDirMock.mockClear(); saveMock.mockResolvedValue('C:\\out\\doc.html'); + getAppDataDirMock.mockResolvedValue('/app/data'); document.body.innerHTML = ''; }); @@ -40,13 +51,10 @@ describe('export', () => { expect(html).toContain('katex'); }); - it('exports pdf via main window.print + overlay, not via saveDialog or rust', async () => { + it('exports pdf by saving html to app data dir and opening in browser', async () => { const root = document.createElement('div'); root.innerHTML = '
Hi
'; - // jsdom 没实现 print,给 window.print 打桩 - const printSpy = vi.spyOn(window, 'print').mockImplementation(() => {}); - await exportDocument({ element: root, source: '# 你好', @@ -55,15 +63,21 @@ describe('export', () => { format: 'pdf', }); + // PDF export does NOT show a save dialog — writes to temp dir instead expect(saveMock).not.toHaveBeenCalled(); - expect(writeFileMock).not.toHaveBeenCalled(); - expect(printSpy).toHaveBeenCalledTimes(1); - - // 验证 overlay 被注入后又清理了 - expect(document.querySelector('.print-overlay')).toBeNull(); - expect(document.getElementById('print-overlay-style')).toBeNull(); - printSpy.mockRestore(); + // Should write standalone HTML (with print CSS + auto-print script) + expect(writeFileMock).toHaveBeenCalledWith( + '/app/data/note.html', + expect.stringContaining(''), + ); + expect(writeFileMock).toHaveBeenCalledWith( + '/app/data/note.html', + expect.stringContaining('window.print()'), + ); + + // Should open the temp HTML in the default browser (via system cmd, not opener plugin) + expect(openFileMock).toHaveBeenCalledWith('/app/data/note.html'); }); it('exports markdown as a new md file', async () => { diff --git a/src/features/export/export.ts b/src/features/export/export.ts index 243f596..b1f600c 100644 --- a/src/features/export/export.ts +++ b/src/features/export/export.ts @@ -1,5 +1,6 @@ import { save as saveDialog } from '@tauri-apps/plugin-dialog'; import { writeFile } from '../../ipc/files'; +import { openFileWithSystem } from '../../ipc/opener'; import themesCss from '../../styles/themes.css?raw'; import proseCss from '../../styles/prose.css?raw'; @@ -51,6 +52,7 @@ export function buildStandaloneHtml( ${BASE_CSS} ${css} ${getActiveThemeCss()} + ${PRINT_CSS} ${contentHtml} @@ -59,10 +61,23 @@ export function buildStandaloneHtml( export async function exportDocument(options: ExportDocumentOptions): Promise