diff --git a/main.ts b/main.ts index ddc8d1b..559718c 100644 --- a/main.ts +++ b/main.ts @@ -1,5 +1,5 @@ 'use strict'; -import { MarkdownView, Notice, Plugin, requestUrl, TFile } from 'obsidian'; +import { MarkdownView, Modal, Notice, Plugin, requestUrl, TFile } from 'obsidian'; import { findLineForAnchor } from './src/anchor'; import { serializeCacheFile, shouldConfirmRegenerate, touchCacheEntry } from './src/cache'; import { CacheManager } from './src/cache-manager'; @@ -208,6 +208,11 @@ class ParallelReaderPlugin extends Plugin { name: this.t('cmdCardJump'), callback: () => this.jumpActiveCard(), }); + this.addCommand({ + id: 'parallel-reader-batch-generate', + name: this.t('cmdBatchGenerate'), + callback: () => this.runBatchForFolder(), + }); this.addSettingTab(new ParallelReaderSettingTab(this.app, this)); @@ -551,6 +556,57 @@ class ParallelReaderPlugin extends Plugin { }); } + async runBatchForFolder() { + const plugin = this; + const folderPath = await new Promise((resolve) => { + class FolderPromptModal extends Modal { + private input!: HTMLInputElement; + onOpen() { + this.contentEl.createEl('p', { text: plugin.t('batchSelectFolder') }); + this.input = this.contentEl.createEl('input', { type: 'text' }); + this.input.placeholder = plugin.t('batchFolderPrompt'); + this.input.style.width = '100%'; + this.input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + resolve(this.input.value.trim()); + this.close(); + } + }); + this.contentEl.createEl('button', { text: 'OK' }).addEventListener('click', () => { + resolve(this.input.value.trim()); + this.close(); + }); + } + onClose() { + resolve(null); + } + } + new FolderPromptModal(plugin.app).open(); + }); + if (folderPath === null) return; + const allFiles = this.app.vault.getMarkdownFiles().filter((f) => { + if (folderPath === '') return !f.path.includes('/'); + return f.parent?.path === folderPath; + }); + if (allFiles.length === 0) { + new Notice(this.t('batchNoMarkdown')); + return; + } + let skipped = 0; + for (let i = 0; i < allFiles.length; i++) { + const file = allFiles[i]; + new Notice(this.t('batchProgress', { current: i + 1, total: allFiles.length })); + const content = await this.app.vault.read(file); + const entry = this.cacheManager.get(file.path); + if (entry && cacheEntryMatches(entry, content, this.settings)) { + skipped++; + continue; + } + await this.runForFile(file, false); + } + new Notice(this.t('batchDone', { total: allFiles.length, skipped })); + } + resolveCardAnchors(content: string, rawCards: RawCard[]) { const resolved: ResolvedCard[] = (rawCards || []).map((c: RawCard) => ({ title: c.title, diff --git a/src/i18n.ts b/src/i18n.ts index 703dada..fa73899 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -145,6 +145,12 @@ export const STRINGS: Record> = { clearAllCacheButton: '清除所有缓存', settingStreamingName: '流式输出', settingStreamingDesc: '启用后生成时实时显示 LLM 输出进度(仅 OpenAI Chat 和 Anthropic 格式支持)', + cmdBatchGenerate: '批量生成对照笔记(当前文件夹)', + batchSelectFolder: '请输入要批量处理的文件夹路径(留空为 Vault 根目录)', + batchFolderPrompt: '文件夹路径', + batchNoMarkdown: '该文件夹下没有 Markdown 文件', + batchProgress: '批量生成:{current}/{total}', + batchDone: '批量生成完成:共处理 {total} 篇,跳过缓存 {skipped} 篇', }, en: { appTitle: 'Parallel Reader', @@ -295,6 +301,12 @@ export const STRINGS: Record> = { settingStreamingName: 'Streaming output', settingStreamingDesc: 'Show real-time LLM output progress during generation (OpenAI Chat and Anthropic formats only)', + cmdBatchGenerate: 'Batch generate parallel notes (current folder)', + batchSelectFolder: 'Enter the folder path to batch-process (leave blank for Vault root)', + batchFolderPrompt: 'Folder path', + batchNoMarkdown: 'No Markdown files found in that folder', + batchProgress: 'Batch generating: {current}/{total}', + batchDone: 'Batch complete: processed {total}, skipped {skipped} (cached)', }, };