/* eslint-disable no-param-reassign */ /* eslint-disable no-underscore-dangle */ import { Editor, MarkdownView, Notice, Plugin } from "obsidian"; import ImageUploader from "./uploader/ImageUploader"; // eslint-disable-next-line import/no-cycle import ImgurPluginSettingsTab from "./ui/ImgurPluginSettingsTab"; import ApiError from "./uploader/ApiError"; import UploadStrategy from "./UploadStrategy"; import buildUploaderFrom from "./uploader/imgUploaderFactory"; import RemoteUploadConfirmationDialog from "./ui/RemoteUploadConfirmationDialog"; import PasteEventCopy from "./aux-event-classes/PasteEventCopy"; import DragEventCopy from "./aux-event-classes/DragEventCopy"; import editorCheckCallbackFor from "./imgur/resizing/plugin-callback"; import ImgurSize from "./imgur/resizing/ImgurSize"; declare module "obsidian" { interface MarkdownSubView { clipboardManager: ClipboardManager; } } interface ClipboardManager { handlePaste(e: ClipboardEvent): void; handleDrop(e: DragEvent): void; } export interface ImgurPluginSettings { uploadStrategy: string; clientId: string; showRemoteUploadConfirmation: boolean; } const DEFAULT_SETTINGS: ImgurPluginSettings = { uploadStrategy: UploadStrategy.ANONYMOUS_IMGUR.id, clientId: null, showRemoteUploadConfirmation: true, }; export default class ImgurPlugin extends Plugin { settings: ImgurPluginSettings; private imgUploaderField: ImageUploader; private customPasteEventCallback = async ( e: ClipboardEvent, _: Editor, markdownView: MarkdownView ) => { if (e instanceof PasteEventCopy) return; if (!this.imgUploader) { ImgurPlugin.showUnconfiguredPluginNotice(); return; } const { files } = e.clipboardData; if (files.length === 0 || !files[0].type.startsWith("image")) { return; } e.preventDefault(); if (this.settings.showRemoteUploadConfirmation) { const modal = new RemoteUploadConfirmationDialog(this.app); modal.open(); const userResp = await modal.response(); switch (userResp.shouldUpload) { case undefined: return; case true: if (userResp.alwaysUpload) { this.settings.showRemoteUploadConfirmation = false; this.saveSettings() .then(() => {}) .catch(() => {}); } break; case false: markdownView.currentMode.clipboardManager.handlePaste( new PasteEventCopy(e) ); return; default: return; } } for (let i = 0; i < files.length; i += 1) { this.uploadFileAndEmbedImgurImage(files[i]).catch(() => { markdownView.currentMode.clipboardManager.handlePaste( new PasteEventCopy(e) ); }); } }; private customDropEventListener = async ( e: DragEvent, _: Editor, markdownView: MarkdownView ) => { if (e instanceof DragEventCopy) return; if (!this.imgUploader) { ImgurPlugin.showUnconfiguredPluginNotice(); return; } if ( e.dataTransfer.types.length !== 1 || e.dataTransfer.types[0] !== "Files" ) { return; } // Preserve files before showing modal, otherwise they will be lost from the event const { files } = e.dataTransfer; e.preventDefault(); if (this.settings.showRemoteUploadConfirmation) { const modal = new RemoteUploadConfirmationDialog(this.app); modal.open(); const userResp = await modal.response(); switch (userResp.shouldUpload) { case undefined: return; case true: if (userResp.alwaysUpload) { this.settings.showRemoteUploadConfirmation = false; this.saveSettings() .then(() => {}) .catch(() => {}); } break; case false: { markdownView.currentMode.clipboardManager.handleDrop( DragEventCopy.create(e, files) ); return; } default: return; } } for (let i = 0; i < files.length; i += 1) { if (!files[i].type.startsWith("image")) { return; } } // Adding newline to avoid messing images pasted via default handler // with any text added by the plugin this.getEditor().replaceSelection("\n"); const promises: Promise<void>[] = []; const filesFailedToUpload: File[] = []; for (let i = 0; i < files.length; i += 1) { const image = files[i]; const uploadPromise = this.uploadFileAndEmbedImgurImage(image).catch( () => { filesFailedToUpload.push(image); } ); promises.push(uploadPromise); } await Promise.all(promises); if (filesFailedToUpload.length === 0) { return; } markdownView.currentMode.clipboardManager.handleDrop( DragEventCopy.create(e, filesFailedToUpload) ); }; get imgUploader(): ImageUploader { return this.imgUploaderField; } private async loadSettings() { this.settings = { ...DEFAULT_SETTINGS, ...((await this.loadData()) as ImgurPluginSettings), }; } async saveSettings(): Promise<void> { await this.saveData(this.settings); } async onload(): Promise<void> { const sizes = ImgurSize.values(); for (let i = 0; i < sizes.length; i += 1) { const size = sizes[i]; this.addCommand({ id: `imgur-resize-${size.suffix}-command`, name: `Resize to ${size.description}${ size.sizeHint ? ` (${size.sizeHint})` : "" }`, editorCheckCallback: editorCheckCallbackFor(size), }); } await this.loadSettings(); this.addSettingTab(new ImgurPluginSettingsTab(this.app, this)); this.setupImgurHandlers(); this.setupImagesUploader(); } setupImagesUploader(): void { this.imgUploaderField = buildUploaderFrom(this.settings); } private setupImgurHandlers() { this.registerEvent( this.app.workspace.on("editor-paste", this.customPasteEventCallback) ); this.registerEvent( this.app.workspace.on("editor-drop", this.customDropEventListener) ); } private static showUnconfiguredPluginNotice() { const fiveSecondsMillis = 5_000; // eslint-disable-next-line no-new new Notice( "⚠️ Please configure Imgur plugin or disable it", fiveSecondsMillis ); } private async uploadFileAndEmbedImgurImage(file: File) { const pasteId = (Math.random() + 1).toString(36).substr(2, 5); this.insertTemporaryText(pasteId); let imgUrl: string; try { imgUrl = await this.imgUploaderField.upload(file); } catch (e) { if (e instanceof ApiError) { this.handleFailedUpload( pasteId, `Upload failed, remote server returned an error: ${e.message}` ); } else { // eslint-disable-next-line no-console console.error("Failed imgur request: ", e); this.handleFailedUpload( pasteId, "⚠️Imgur upload failed, check dev console" ); } throw e; } this.embedMarkDownImage(pasteId, imgUrl); } private insertTemporaryText(pasteId: string) { const progressText = ImgurPlugin.progressTextFor(pasteId); this.getEditor().replaceSelection(`${progressText}\n`); } private static progressTextFor(id: string) { return `![Uploading file...${id}]()`; } private embedMarkDownImage(pasteId: string, imageUrl: string) { const progressText = ImgurPlugin.progressTextFor(pasteId); const markDownImage = `![](${imageUrl})`; ImgurPlugin.replaceFirstOccurrence( this.getEditor(), progressText, markDownImage ); } private handleFailedUpload(pasteId: string, message: string) { const progressText = ImgurPlugin.progressTextFor(pasteId); ImgurPlugin.replaceFirstOccurrence( this.getEditor(), progressText, `<!--${message}-->` ); } private getEditor(): Editor { const mdView = this.app.workspace.activeLeaf.view as MarkdownView; return mdView.editor; } private static replaceFirstOccurrence( editor: Editor, target: string, replacement: string ) { const lines = editor.getValue().split("\n"); for (let i = 0; i < lines.length; i += 1) { const ch = lines[i].indexOf(target); if (ch !== -1) { const from = { line: i, ch }; const to = { line: i, ch: ch + target.length }; editor.replaceRange(replacement, from, to); break; } } } }