diff --git a/main.ts b/main.ts index 69fa95d..e6c5e6a 100644 --- a/main.ts +++ b/main.ts @@ -25,6 +25,7 @@ import { FileConvertModal } from './src/modals/FileConvertModal'; import { FolderConvertModal } from './src/modals/FolderConvertModal'; import { UrlConvertModal } from './src/modals/UrlConvertModal'; import { SetupModal } from './src/modals/SetupModal'; +import { PreviewModal } from './src/modals/PreviewModal'; export default class MarkitdownPlugin extends Plugin { settings: MarkitdownSettings = DEFAULT_SETTINGS; @@ -245,11 +246,30 @@ export default class MarkitdownPlugin extends Plugin { const result = await this.converter.convert(inputPath, outputPath, options); if (result.success) { - const msg = result.imagesExtracted - ? `Converted successfully (${result.imagesExtracted} images extracted)` - : 'Converted successfully'; - new Notice(msg); - await this.openConvertedFile(outputPath, vaultPath); + let content: string; + try { + content = await fs.promises.readFile(outputPath, 'utf-8'); + } catch { + new Notice('Conversion succeeded but could not read output file'); + return; + } + + new PreviewModal(this.app, { + content, + outputPath, + processingTime: result.processingTime ?? 0, + onSave: async () => { + const msg = result.imagesExtracted + ? `Converted successfully (${result.imagesExtracted} images extracted)` + : 'Converted successfully'; + new Notice(msg); + await this.openConvertedFile(outputPath, vaultPath); + }, + onCancel: async () => { + await fs.promises.unlink(outputPath).catch(() => {}); + new Notice('Conversion discarded'); + }, + }).open(); } else { new Notice(`Conversion failed: ${result.error}`); } diff --git a/src/modals/FileConvertModal.ts b/src/modals/FileConvertModal.ts index 7d09772..7b10ac5 100644 --- a/src/modals/FileConvertModal.ts +++ b/src/modals/FileConvertModal.ts @@ -4,6 +4,7 @@ import * as fs from 'fs'; import type MarkitdownPlugin from '../../main'; import { FILE_INPUT_ACCEPT } from '../utils/fileTypes'; import { getVaultBasePath, resolveOutputFolder, resolveFilenameTemplate, toVaultRelative } from '../utils/paths'; +import { PreviewModal } from './PreviewModal'; export class FileConvertModal extends Modal { private plugin: MarkitdownPlugin; @@ -67,12 +68,36 @@ export class FileConvertModal extends Modal { const result = await this.plugin.convertExternalFile(tempFilePath, outputPath); if (result.success) { - const msg = result.imagesExtracted - ? `Converted successfully (${result.imagesExtracted} images extracted)` - : 'Converted successfully'; - new Notice(msg); + // Read the converted output for preview + let content: string; + try { + content = await fs.promises.readFile(outputPath, 'utf-8'); + } catch { + new Notice('Conversion succeeded but could not read output file'); + convertButton.disabled = false; + convertButton.setText('Convert'); + return; + } + this.close(); - await this.plugin.openConvertedFile(outputPath, vaultPath); + + new PreviewModal(this.app, { + content, + outputPath, + processingTime: result.processingTime ?? 0, + onSave: async () => { + const msg = result.imagesExtracted + ? `Converted successfully (${result.imagesExtracted} images extracted)` + : 'Converted successfully'; + new Notice(msg); + await this.plugin.openConvertedFile(outputPath, vaultPath); + }, + onCancel: async () => { + // Discard the converted output file + await fs.promises.unlink(outputPath).catch(() => {}); + new Notice('Conversion discarded'); + }, + }).open(); } else { new Notice(`Conversion failed: ${result.error}`); convertButton.disabled = false; diff --git a/src/modals/PreviewModal.ts b/src/modals/PreviewModal.ts new file mode 100644 index 0000000..707d8a4 --- /dev/null +++ b/src/modals/PreviewModal.ts @@ -0,0 +1,97 @@ +import { App, Modal } from 'obsidian'; + +const MAX_PREVIEW_LINES = 500; + +export class PreviewModal extends Modal { + private content: string; + private outputPath: string; + private processingTime: number; + private onSave: () => Promise | void; + private onCancel: () => Promise | void; + + constructor( + app: App, + opts: { + content: string; + outputPath: string; + processingTime: number; + onSave: () => Promise | void; + onCancel: () => Promise | void; + } + ) { + super(app); + this.content = opts.content; + this.outputPath = opts.outputPath; + this.processingTime = opts.processingTime; + this.onSave = opts.onSave; + this.onCancel = opts.onCancel; + } + + onOpen() { + const { contentEl } = this; + contentEl.addClass('markitdown-modal'); + contentEl.addClass('markitdown-preview-modal'); + contentEl.createEl('h2', { text: 'Conversion preview' }); + + // Metadata section + const metaEl = contentEl.createDiv('markitdown-preview-meta'); + metaEl.createEl('div', { + text: `Output: ${this.outputPath}`, + cls: 'markitdown-preview-meta-item', + }); + metaEl.createEl('div', { + text: `Processing time: ${(this.processingTime / 1000).toFixed(1)}s`, + cls: 'markitdown-preview-meta-item', + }); + metaEl.createEl('div', { + text: `Content length: ${this.content.length.toLocaleString()} characters`, + cls: 'markitdown-preview-meta-item', + }); + + // Determine if content should be truncated + const lines = this.content.split('\n'); + const truncated = lines.length > MAX_PREVIEW_LINES; + const displayContent = truncated + ? lines.slice(0, MAX_PREVIEW_LINES).join('\n') + : this.content; + + if (truncated) { + contentEl.createEl('div', { + text: `Showing first ${MAX_PREVIEW_LINES} of ${lines.length} lines`, + cls: 'markitdown-preview-truncation-note', + }); + } + + // Scrollable preview area + const previewContainer = contentEl.createDiv('markitdown-preview-content'); + previewContainer.createEl('pre', { text: displayContent }); + + // Buttons + const buttonContainer = contentEl.createDiv('markitdown-button-container'); + + const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel' }); + cancelBtn.addEventListener('click', async () => { + await this.onCancel(); + this.close(); + }); + + const saveBtn = buttonContainer.createEl('button', { + text: 'Save', + cls: 'mod-cta', + }); + saveBtn.addEventListener('click', async () => { + saveBtn.disabled = true; + saveBtn.setText('Saving...'); + cancelBtn.disabled = true; + try { + await this.onSave(); + } finally { + this.close(); + } + }); + } + + onClose() { + this.contentEl.empty(); + } +} diff --git a/styles.css b/styles.css index 2532999..5b4c07e 100644 --- a/styles.css +++ b/styles.css @@ -176,6 +176,7 @@ Styles for the Markitdown Obsidian plugin font-weight: 500; } +<<<<<<< HEAD /* Resolved path hint */ .markitdown-resolved-path-hint { font-size: 0.85em; @@ -285,6 +286,51 @@ Styles for the Markitdown Obsidian plugin color: var(--text-faint); } +/* Preview modal */ +.markitdown-preview-modal { + width: 80vw; + max-width: 900px; +} + +.markitdown-preview-meta { + background-color: var(--background-secondary); + padding: 10px 14px; + border-radius: 6px; + margin-bottom: 12px; +} + +.markitdown-preview-meta-item { + font-size: 0.85em; + color: var(--text-muted); + padding: 2px 0; +} + +.markitdown-preview-truncation-note { + font-size: 0.85em; + color: var(--text-accent); + margin-bottom: 8px; + font-style: italic; +} + +.markitdown-preview-content { + max-height: 50vh; + overflow-y: auto; + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + background-color: var(--background-primary-alt); + padding: 12px; + margin-bottom: 16px; +} + +.markitdown-preview-content pre { + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; + font-family: var(--font-monospace); + font-size: 0.85em; + color: var(--text-normal); +} + /* Plugin args editor */ .markitdown-plugin-args-container { margin: 8px 0 16px;