From 28780173d95f169e56bb369d32aab70002964c5d Mon Sep 17 00:00:00 2001 From: Ethan Troy Date: Tue, 17 Mar 2026 20:43:10 -0400 Subject: [PATCH] Add drag-and-drop file conversion (ENG-641) --- main.ts | 88 ++++++++++++++++++++++++++++++++++++- src/settings/SettingsTab.ts | 10 +++++ src/types/settings.ts | 2 + 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/main.ts b/main.ts index a700c40..1eeddc3 100644 --- a/main.ts +++ b/main.ts @@ -1,5 +1,6 @@ -import { Notice, Plugin, TFile } from 'obsidian'; +import { Notice, Plugin, TFile, MarkdownView, MarkdownFileInfo, Editor } from 'obsidian'; import * as path from 'path'; +import * as fs from 'fs'; import { MarkitdownSettings, DEFAULT_SETTINGS, @@ -66,6 +67,11 @@ export default class MarkitdownPlugin extends Plugin { this.registerFileMenu(); } + // Drag-and-drop conversion + if (this.settings.enableDragDrop) { + this.registerDropHandler(); + } + // Settings tab this.addSettingTab(new SettingsTab(this.app, this)); } @@ -108,6 +114,86 @@ export default class MarkitdownPlugin extends Plugin { ); } + /** Register drag-and-drop handler for converting dropped files. */ + private registerDropHandler() { + this.registerEvent( + this.app.workspace.on('editor-drop', (evt: DragEvent, editor: Editor, info: MarkdownView | MarkdownFileInfo) => { + const files = evt.dataTransfer?.files; + if (!files || files.length === 0) return; + + // Check if any dropped file is convertible + const convertibleFiles: File[] = []; + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const ext = path.extname(file.name).toLowerCase().replace(/^\./, ''); + if (isConvertible(ext)) { + convertibleFiles.push(file); + } + } + + if (convertibleFiles.length === 0) return; + + // Intercept the event for convertible files + evt.preventDefault(); + + // Process each convertible file asynchronously + for (const file of convertibleFiles) { + this.handleDroppedFile(file, editor).catch((error) => { + const msg = error instanceof Error ? error.message : String(error); + new Notice(`Drop conversion error: ${msg}`); + }); + } + }) + ); + } + + /** Handle a single dropped file: write to temp, convert, insert link, clean up. */ + private async handleDroppedFile(file: File, editor: Editor): Promise { + const vaultPath = getVaultBasePath(this.app); + if (!vaultPath) { + new Notice('Could not determine vault path. This plugin requires a local vault.'); + return; + } + + if (!this.dependencyStatus.markitdownInstalled) { + new SetupModal(this.app, this).open(); + return; + } + + new Notice(`Converting dropped file: ${file.name}...`); + + const outputFolder = resolveOutputFolder(vaultPath, this.settings.outputPath); + const baseName = path.basename(file.name, path.extname(file.name)); + const outputPath = path.join(outputFolder, `${baseName}.md`); + + // Write the DOM File to a temp file on disk + const safeName = file.name.replace(/[^a-zA-Z0-9._-]/g, '_'); + const tempFilePath = path.join(outputFolder, `tmp_${Date.now()}_${safeName}`); + const buffer = await file.arrayBuffer(); + await fs.promises.writeFile(tempFilePath, Buffer.from(buffer)); + + try { + const result = await this.convertExternalFile(tempFilePath, outputPath); + + if (result.success) { + const relativePath = toVaultRelative(outputPath, vaultPath); + const linkText = `[[${relativePath.replace(/\.md$/, '')}]]`; + const cursor = editor.getCursor(); + editor.replaceRange(linkText, cursor); + + const msg = result.imagesExtracted + ? `Converted ${file.name} (${result.imagesExtracted} images extracted)` + : `Converted ${file.name} successfully`; + new Notice(msg); + } else { + new Notice(`Conversion failed for ${file.name}: ${result.error}`); + } + } finally { + // Clean up temp file + await fs.promises.unlink(tempFilePath).catch(() => {}); + } + } + /** Convert a file that already exists in the vault (from context menu). */ async convertVaultFile(file: TFile): Promise { const vaultPath = getVaultBasePath(this.app); diff --git a/src/settings/SettingsTab.ts b/src/settings/SettingsTab.ts index 7cc2a42..09bf56f 100644 --- a/src/settings/SettingsTab.ts +++ b/src/settings/SettingsTab.ts @@ -120,6 +120,16 @@ export class SettingsTab extends PluginSettingTab { await this.plugin.saveSettings(); })); + new Setting(containerEl) + .setName('Drag and drop') + .setDesc('Convert supported files automatically when dropped into the editor') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.enableDragDrop) + .onChange(async (value) => { + this.plugin.settings.enableDragDrop = value; + await this.plugin.saveSettings(); + })); + // ── Advanced ───────────────────────────── new Setting(containerEl) .setName('Advanced') diff --git a/src/types/settings.ts b/src/types/settings.ts index a2e1e8d..5eeafe5 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -13,6 +13,7 @@ export interface MarkitdownSettings { imageSubfolderTemplate: string; enableBatchProgress: boolean; enableContextMenu: boolean; + enableDragDrop: boolean; } export const DEFAULT_SETTINGS: MarkitdownSettings = { @@ -25,6 +26,7 @@ export const DEFAULT_SETTINGS: MarkitdownSettings = { imageSubfolderTemplate: '{filename}-images', enableBatchProgress: true, enableContextMenu: true, + enableDragDrop: true, }; export interface ConversionOptions {