From d4515272e24ee98e2bc810d899e4759efbeb29a6 Mon Sep 17 00:00:00 2001 From: RyotaUshio Date: Tue, 22 Oct 2024 15:41:50 +0900 Subject: [PATCH] release: 0.40.12 --- .eslintrc | 8 +- manifest-beta.json | 2 +- manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/auto-copy.ts | 2 +- src/backlink-visualizer.ts | 2 +- src/bib.ts | 2 +- src/color-palette.ts | 6 +- src/context-menu.ts | 34 +-- src/dom-manager.ts | 2 +- src/drag.ts | 18 +- src/lib/commands.ts | 36 +-- src/lib/composer.ts | 14 +- src/lib/copy-link.ts | 16 +- src/lib/dummy-file-manager.ts | 134 ++++++++++ src/lib/highlights/write-file/index.ts | 2 +- src/lib/index.ts | 31 ++- src/lib/name-or-number-trees.ts | 6 +- src/lib/outlines.ts | 2 +- src/lib/pdf-backlink-index.ts | 4 +- src/lib/workspace-lib.ts | 4 +- src/main.ts | 26 +- src/modals/annotation-modals.ts | 10 +- src/modals/dummy-file-modals.ts | 262 +++++++++++++++++++ src/modals/external-pdf-modals.ts | 345 ------------------------- src/modals/index.ts | 2 +- src/modals/outline-modals.ts | 2 +- src/modals/page-label-modals.ts | 18 +- src/modals/pdf-composer-modals.ts | 6 +- src/modals/restore-default-modal.ts | 43 +++ src/patchers/backlink.ts | 6 +- src/patchers/clipboard-manager.ts | 6 +- src/patchers/menu.ts | 6 +- src/patchers/page-preview.ts | 4 +- src/patchers/pdf-embed.ts | 2 +- src/patchers/pdf-internals.ts | 78 +++--- src/patchers/pdf-outline-viewer.ts | 4 +- src/patchers/pdf-view.ts | 8 +- src/patchers/workspace.ts | 4 +- src/pdf-backlink.ts | 6 +- src/post-process/pdf-link-like.ts | 2 +- src/settings.ts | 311 +++++++++++++++++----- src/toolbar.ts | 2 +- src/typings.d.ts | 4 +- src/user-script/context.ts | 4 +- src/utils/color.ts | 4 +- src/utils/events.ts | 16 +- src/utils/index.ts | 14 +- src/utils/maps.ts | 2 +- src/utils/menu.ts | 4 +- src/utils/suggest.ts | 6 +- src/vim/ex-commands.ts | 8 +- src/vim/hint.ts | 2 +- src/vim/hintnames.ts | 4 +- src/vim/outline.ts | 2 +- src/vim/scope.ts | 12 +- src/vim/scroll.ts | 4 +- src/vim/vim.ts | 4 +- src/vim/visual.ts | 4 +- styles.css | 4 + 61 files changed, 952 insertions(+), 632 deletions(-) create mode 100644 src/lib/dummy-file-manager.ts create mode 100644 src/modals/dummy-file-modals.ts delete mode 100644 src/modals/external-pdf-modals.ts create mode 100644 src/modals/restore-default-modal.ts diff --git a/.eslintrc b/.eslintrc index 0b5a883e..5a138bc6 100644 --- a/.eslintrc +++ b/.eslintrc @@ -22,6 +22,12 @@ "no-prototype-builtins": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-this-alias": "off", - "@typescript-eslint/no-inferrable-types": ["error", { "ignoreParameters": true, "ignoreProperties": true }] + "@typescript-eslint/no-inferrable-types": ["error", { "ignoreParameters": true, "ignoreProperties": true }], + "semi": ["error", "always"], + "semi-spacing": ["error", {"after": true, "before": false}], + "semi-style": ["error", "last"], + "no-extra-semi": "error", + "no-unexpected-multiline": "error", + "no-unreachable": "error" } } \ No newline at end of file diff --git a/manifest-beta.json b/manifest-beta.json index 06236304..fbcfff67 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "pdf-plus", "name": "PDF++", - "version": "0.40.11", + "version": "0.40.12", "minAppVersion": "1.5.8", "description": "The most Obsidian-native PDF annotation tool ever.", "author": "Ryota Ushio", diff --git a/manifest.json b/manifest.json index 06236304..fbcfff67 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "pdf-plus", "name": "PDF++", - "version": "0.40.11", + "version": "0.40.12", "minAppVersion": "1.5.8", "description": "The most Obsidian-native PDF annotation tool ever.", "author": "Ryota Ushio", diff --git a/package-lock.json b/package-lock.json index b975d9bf..3c167475 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-pdf-plus", - "version": "0.40.11", + "version": "0.40.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-pdf-plus", - "version": "0.40.11", + "version": "0.40.12", "license": "MIT", "devDependencies": { "@cantoo/pdf-lib": "^1.21.1", diff --git a/package.json b/package.json index a521bffd..51a31359 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-pdf-plus", - "version": "0.40.11", + "version": "0.40.12", "description": "The most Obsidian-native PDF annotation tool ever.", "scripts": { "dev": "node esbuild.config.mjs", diff --git a/src/auto-copy.ts b/src/auto-copy.ts index c7905c32..d5d5bbd2 100644 --- a/src/auto-copy.ts +++ b/src/auto-copy.ts @@ -33,7 +33,7 @@ export class AutoCopyMode extends PDFPlusComponent { this.plugin.openSettingTab().scrollToHeading('auto-copy'); }); }); - menu.onHide(() => { menuShown = false }); + menu.onHide(() => { menuShown = false; }); menu.showAtMouseEvent(evt); menuShown = true; }); diff --git a/src/backlink-visualizer.ts b/src/backlink-visualizer.ts index 2b9038ae..296e6de8 100644 --- a/src/backlink-visualizer.ts +++ b/src/backlink-visualizer.ts @@ -162,7 +162,7 @@ export class BacklinkDomManager extends PDFPlusComponent { backlinkItemEl.removeClass('hovered-backlink'); el.removeEventListener('mouseout', listener); } - } + }; el.addEventListener('mouseout', listener); } }); diff --git a/src/bib.ts b/src/bib.ts index 1f58af90..9d0a2e21 100644 --- a/src/bib.ts +++ b/src/bib.ts @@ -91,7 +91,7 @@ export class BibliographyManager extends PDFPlusComponent { hoverPopover.addChild( new BibliographyDom(this, destId, bibContainerEl) ); - } + }; if (this.plugin.requireModKeyForLinkHover(BibliographyManager.HOVER_LINK_SOURCE_ID)) { onModKeyPress(event, targetEl, spawnBibPopover); diff --git a/src/color-palette.ts b/src/color-palette.ts index d0352414..f71c1257 100644 --- a/src/color-palette.ts +++ b/src/color-palette.ts @@ -234,7 +234,7 @@ export class ColorPalette extends PDFPlusComponent { addCopyActionDropdown(paletteEl: HTMLElement) { let tooltip = 'Link copy format'; if (!this.plugin.settings.colorPaletteInToolbar) { - tooltip = `${this.plugin.manifest.name}: link copy options (trigger via hotkeys)` + tooltip = `${this.plugin.manifest.name}: link copy options (trigger via hotkeys)`; } const buttonEl = this.addDropdown( @@ -427,7 +427,7 @@ export class ColorPalette extends PDFPlusComponent { addCropButton(paletteEl: HTMLElement) { this.cropButtonEl = paletteEl.createDiv('clickable-icon pdf-plus-rect-select', (el) => { setIcon(el, 'lucide-box-select'); - setTooltip(el, 'Copy embed link to rectangular selection') + setTooltip(el, 'Copy embed link to rectangular selection'); el.addEventListener('click', () => { this.startRectangularSelection(false); @@ -470,7 +470,7 @@ export class ColorPalette extends PDFPlusComponent { // Determine the target page based on the event target if (!(isTargetHTMLElement(evt, evt.target))) return; - const pageEl = evt.target.closest('.pdf-viewer div.page[data-page-number]') + const pageEl = evt.target.closest('.pdf-viewer div.page[data-page-number]'); if (!pageEl) return; const pageNumber = pageEl.dataset.pageNumber; diff --git a/src/context-menu.ts b/src/context-menu.ts index 97827841..66bca86b 100644 --- a/src/context-menu.ts +++ b/src/context-menu.ts @@ -39,7 +39,7 @@ export const onContextMenu = async (plugin: PDFPlus, child: PDFViewerChild, evt: if (!evt.defaultPrevented) { await showContextMenu(plugin, child, evt); } -} +}; export async function showContextMenu(plugin: PDFPlus, child: PDFViewerChild, evt: MouseEvent) { const menu = await PDFPlusContextMenu.fromMouseEvent(plugin, child, evt); @@ -65,7 +65,7 @@ export async function showContextMenuAtSelection(plugin: PDFPlus, child: PDFView const { x, y } = range.getBoundingClientRect(); const menu = new PDFPlusContextMenu(plugin, child); - await menu.addItems() + await menu.addItems(); child.clearEphemeralUI(); plugin.shownMenus.forEach((menu) => menu.hide()); menu.showAtPosition({ x, y }, doc); @@ -94,7 +94,7 @@ export const onThumbnailContextMenu = (plugin: PDFPlus, child: PDFViewerChild, e (evt.view ?? activeWindow).navigator.clipboard.writeText(link); const file = child.file; if (file) plugin.lastCopiedDestInfo = { file, destArray: [pageNumber - 1, 'XYZ', null, null, null] }; - }) + }); }); if (lib.isEditable(child)) { @@ -108,7 +108,7 @@ export const onThumbnailContextMenu = (plugin: PDFPlus, child: PDFViewerChild, e return; } lib.commands._insertPage(file, pageNumber, pageNumber); - }) + }); }) .addItem((item) => { item.setTitle('Insert page after this page') @@ -120,7 +120,7 @@ export const onThumbnailContextMenu = (plugin: PDFPlus, child: PDFViewerChild, e return; } lib.commands._insertPage(file, pageNumber + 1, pageNumber); - }) + }); }) .addItem((item) => { item.setTitle('Delete page') @@ -132,7 +132,7 @@ export const onThumbnailContextMenu = (plugin: PDFPlus, child: PDFViewerChild, e return; } lib.commands._deletePage(file, pageNumber); - }) + }); }) .addItem((item) => { item.setTitle('Extract page to new file') @@ -166,12 +166,12 @@ export const onThumbnailContextMenu = (plugin: PDFPlus, child: PDFViewerChild, e .onClick(() => { plugin.openSettingTab().scrollToHeading('thumbnail'); }); - }) + }); } menu.showAtMouseEvent(evt); } -} +}; // TODO: split into smaller methods export const onOutlineItemContextMenu = (plugin: PDFPlus, child: PDFViewerChild, file: TFile, item: PDFOutlineTreeNode, evt: MouseEvent) => { @@ -201,7 +201,7 @@ export const onOutlineItemContextMenu = (plugin: PDFPlus, child: PDFViewerChild, const destArray = lib.normalizePDFJsDestArray(dest, pageNumber); plugin.lastCopiedDestInfo = { file, destArray }; } - }) + }); }); if (lib.isEditable(child)) { @@ -375,7 +375,7 @@ export const onOutlineItemContextMenu = (plugin: PDFPlus, child: PDFViewerChild, } menu.showAtMouseEvent(evt); -} +}; export const onOutlineContextMenu = (plugin: PDFPlus, child: PDFViewerChild, file: TFile, evt: MouseEvent) => { @@ -410,7 +410,7 @@ export const onOutlineContextMenu = (plugin: PDFPlus, child: PDFViewerChild, fil }) .showAtMouseEvent(evt); } -} +}; export class PDFPlusMenu extends Menu { @@ -435,7 +435,7 @@ export class PDFPlusMenu extends Menu { } export class PDFPlusContextMenu extends PDFPlusMenu { - child: PDFViewerChild + child: PDFViewerChild; constructor(plugin: PDFPlus, child: PDFViewerChild) { super(plugin); @@ -474,7 +474,7 @@ export class PDFPlusContextMenu extends PDFPlusMenu { const isVisible = (id: string) => { return this.settings.contextMenuConfig.find((section) => section.id === id)?.visible; - } + }; // If macOS, add "look up selection" action if (Platform.isMacOS && Platform.isDesktopApp && this.win.electron && selectedText && isVisible('action')) { @@ -660,7 +660,7 @@ export class PDFPlusContextMenu extends PDFPlusMenu { .onClick(() => { navigator.clipboard.writeText(url); }); - }) + }); } } } @@ -722,7 +722,7 @@ export class PDFPlusContextMenu extends PDFPlusMenu { // How does the electron version differ? navigator.clipboard.writeText(annotatedText!); }); - }) + }); } if (selectedText && selection && isVisible('search')) { @@ -887,13 +887,13 @@ export class PDFPlusProductMenuComponent extends PDFPlusComponent { if (this.section && menu === this.rootMenu) item.setSection(this.section); - this.itemToColorName.set(item, i >= 0 ? colorNames[i] : null) + this.itemToColorName.set(item, i >= 0 ? colorNames[i] : null); const hex = this.settings.colors[i >= 0 ? colorNames[i] : 'transparent']; item.dom.addClass('pdf-plus-color-menu-item'); item.titleEl.before(createDiv('pdf-plus-color-indicator', (el) => { el.setCssStyles({ backgroundColor: hex }); - })) + })); }); } diff --git a/src/dom-manager.ts b/src/dom-manager.ts index b64a76c2..a4042e4e 100644 --- a/src/dom-manager.ts +++ b/src/dom-manager.ts @@ -236,7 +236,7 @@ class PDFPlusCalloutRenderer extends MarkdownRenderChild { onload() { const metadata = this.containerEl.dataset.calloutMetadata; if (metadata) { - const rgb = metadata.split(',').map((val) => parseInt(val)) + const rgb = metadata.split(',').map((val) => parseInt(val)); const isRgb = rgb.length === 3 && rgb.every((val) => 0 <= val && val <= 255); if (isRgb) { diff --git a/src/drag.ts b/src/drag.ts index 6ebcad93..eedff468 100644 --- a/src/drag.ts +++ b/src/drag.ts @@ -34,7 +34,7 @@ export const registerOutlineDrag = async (plugin: PDFPlus, pdfOutlineViewer: PDF title, getText: textGenerator, item - } + }; }); app.dragManager.handleDrop(item.selfEl, (evt, draggable, dragging) => { @@ -74,10 +74,10 @@ export const registerOutlineDrag = async (plugin: PDFPlus, pdfOutlineViewer: PDF dropEffect: 'move', hoverEl: item.el, hoverClass: 'is-being-dragged-over', - } + }; } }, false); - })()) + })()); } await Promise.all(promises); @@ -116,10 +116,10 @@ export const registerOutlineDrag = async (plugin: PDFPlus, pdfOutlineViewer: PDF dropEffect: 'move', hoverEl: pdfOutlineViewer.childrenEl, hoverClass: 'is-being-dragged-over', - } + }; } }, false); -} +}; export const registerThumbnailDrag = (plugin: PDFPlus, child: PDFViewerChild, file: TFile) => { const { app, lib } = plugin; @@ -150,11 +150,11 @@ export const registerThumbnailDrag = (plugin: PDFPlus, child: PDFViewerChild, fi file, pageNumber, `#page=${pageNumber}`, '', '', sourcePath ); } - } + }; }); }); -} +}; export const registerAnnotationPopupDrag = (plugin: PDFPlus, popupEl: HTMLElement, child: PDFViewerChild, file: TFile, page: number, id: string) => { const { app, lib } = plugin; @@ -177,7 +177,7 @@ export const registerAnnotationPopupDrag = (plugin: PDFPlus, popupEl: HTMLElemen getText: (sourcePath: string) => { return lib.copyLink.getTextToCopy(child, template, undefined, file, page, `#page=${page}&annotation=${id}`, text ?? '', '', sourcePath); } - } + }; }); }); -} +}; diff --git a/src/lib/commands.ts b/src/lib/commands.ts index 7ae7ed8d..3e02b546 100644 --- a/src/lib/commands.ts +++ b/src/lib/commands.ts @@ -1,7 +1,7 @@ import { Command, MarkdownView, Notice, TFile, normalizePath, setIcon } from 'obsidian'; import { PDFPlusLibSubmodule } from './submodule'; -import { PDFComposerModal, PDFCreateModal, PDFPageDeleteModal, PDFPageLabelEditModal, PDFOutlineTitleModal, ExternalPDFModal } from 'modals'; +import { PDFComposerModal, PDFCreateModal, PDFPageDeleteModal, PDFPageLabelEditModal, PDFOutlineTitleModal, DummyFileModal } from 'modals'; import { PDFOutlines } from './outlines'; import { TemplateProcessor } from 'template'; import { parsePDFSubpath } from 'utils'; @@ -9,6 +9,7 @@ import { DestArray } from 'typings'; import { PDFPlusSettingTab } from 'settings'; import { SidebarView } from 'pdfjs-enums'; import { showContextMenuAtSelection } from 'context-menu'; +import { RestoreDefaultModal } from 'modals/restore-default-modal'; export class PDFPlusCommands extends PDFPlusLibSubmodule { @@ -187,6 +188,10 @@ export class PDFPlusCommands extends PDFPlusLibSubmodule { id: 'create-dummy', name: 'Create dummy file for external PDF', callback: () => this.createDummyForExternalPDF() + }, { + id: 'restore-default', + name: 'Restore default settings', + callback: () => (new RestoreDefaultModal(this.plugin)).open() } ]; @@ -513,10 +518,11 @@ export class PDFPlusCommands extends PDFPlusLibSubmodule { } addPage(checking: boolean) { - if (!this.lib.composer.isEnabled()) return false; + const child = this.lib.getPDFViewerChild(true); + if (!child || !this.lib.isEditable(child)) return false; - const file = this.app.workspace.getActiveFile(); - if (!file || file.extension !== 'pdf') return false; + const file = child.file; + if (!file) return false; if (!checking) this.lib.composer.addPage(file); @@ -524,11 +530,11 @@ export class PDFPlusCommands extends PDFPlusLibSubmodule { } insertPage(checking: boolean, before: boolean) { - if (!this.lib.composer.isEnabled()) return false; - const view = this.lib.workspace.getActivePDFView(); if (!view || !view.file) return false; const file = view.file; + const child = view.viewer.child; + if (!child || !this.lib.isEditable(child)) return false; const basePage = view.getState().page; const page = basePage + (before ? 0 : 1); @@ -558,11 +564,11 @@ export class PDFPlusCommands extends PDFPlusLibSubmodule { } deletePage(checking: boolean) { - if (!this.lib.composer.isEnabled()) return false; - const view = this.lib.workspace.getActivePDFView(); if (!view || !view.file) return false; const file = view.file; + const child = view.viewer.child; + if (!child || !this.lib.isEditable(child)) return false; const page = view.getState().page; @@ -590,12 +596,12 @@ export class PDFPlusCommands extends PDFPlusLibSubmodule { } extractThisPage(checking: boolean) { - if (!this.lib.composer.isEnabled()) return false; - const view = this.lib.workspace.getActivePDFView(); if (!view) return false; const file = view.file; if (!file) return false; + const child = view.viewer.child; + if (!child || !this.lib.isEditable(child)) return false; if (!checking) { const page = view.getState().page; @@ -633,12 +639,12 @@ export class PDFPlusCommands extends PDFPlusLibSubmodule { } dividePDF(checking: boolean) { - if (!this.lib.composer.isEnabled()) return false; - const view = this.lib.workspace.getActivePDFView(); if (!view) return false; const file = view.file; if (!file) return false; + const child = view.viewer.child; + if (!child || !this.lib.isEditable(child)) return false; if (!checking) { const page = view.getState().page; @@ -897,7 +903,7 @@ export class PDFPlusCommands extends PDFPlusLibSubmodule { // See the docstring of getTextToCopy for more details. comment ); - }) + }); }); navigator.clipboard.writeText(data); @@ -911,7 +917,7 @@ export class PDFPlusCommands extends PDFPlusLibSubmodule { copyDebugInfo() { const settings = Object.assign({}, this.settings, { author: '*'.repeat(this.settings.author.length) }); // @ts-ignore - const fullStyleSettings = this.app.plugins.plugins['obsidian-style-settings']?.settingsManager.settings + const fullStyleSettings = this.app.plugins.plugins['obsidian-style-settings']?.settingsManager.settings; const styleSettings = fullStyleSettings ? Object.fromEntries( Object.entries(fullStyleSettings) .filter(([key]) => key.startsWith('pdf-plus@@')) @@ -979,7 +985,7 @@ export class PDFPlusCommands extends PDFPlusLibSubmodule { } createDummyForExternalPDF() { - new ExternalPDFModal(this.plugin).open(); + new DummyFileModal(this.plugin).open(); } showContextMenu(checking: boolean) { diff --git a/src/lib/composer.ts b/src/lib/composer.ts index 35e266e9..98093721 100644 --- a/src/lib/composer.ts +++ b/src/lib/composer.ts @@ -31,7 +31,7 @@ export class PDFComposer extends PDFPlusLibSubmodule { return await this.linkUpdater.updateLinks( () => this.fileOperator.addPage(file), [file], - (f, n) => { return {} } + (f, n) => { return {}; } ); } @@ -39,7 +39,7 @@ export class PDFComposer extends PDFPlusLibSubmodule { return await this.linkUpdater.updateLinks( () => this.fileOperator.insertPage(file, pageNumber, basePageNumber, keepLabels), [file], - (f, n) => { return { pageNumber: typeof n === 'number' && n >= pageNumber ? n + 1 : n } } + (f, n) => { return { pageNumber: typeof n === 'number' && n >= pageNumber ? n + 1 : n }; } ); } @@ -47,7 +47,7 @@ export class PDFComposer extends PDFPlusLibSubmodule { return await this.linkUpdater.updateLinks( () => this.fileOperator.removePage(file, pageNumber, keepLabels), [file], - (f, n) => { return { pageNumber: typeof n === 'number' && n > pageNumber ? n - 1 : n } } + (f, n) => { return { pageNumber: typeof n === 'number' && n > pageNumber ? n - 1 : n }; } ); } @@ -191,7 +191,7 @@ export class PDFFileOperator extends PDFPlusLibSubmodule { const resultFile = await this.write(file1.path, doc1, true); if (resultFile === null) return null; - await this.app.vault.delete(file2); + await this.app.fileManager.trashFile(file2); return resultFile; } @@ -214,7 +214,7 @@ export class PDFFileOperator extends PDFPlusLibSubmodule { ]); // Get the pages to keep in the source file (pages not in the `pages` array) - const srcPages = [] + const srcPages = []; for (let page = 1; page <= srcDoc.getPageCount(); page++) { if (!pages.includes(page)) srcPages.push(page); } @@ -257,7 +257,7 @@ export class PDFFileOperator extends PDFPlusLibSubmodule { const doc = await this.read(srcFile); // Get the pages not to include in the resulting document (pages not in the `pages` array) - const pagesToRemove = [] + const pagesToRemove = []; for (let page = 1; page <= doc.getPageCount(); page++) { if (!pages.includes(page)) pagesToRemove.push(page); } @@ -336,7 +336,7 @@ export class PDFLinkUpdater extends PDFPlusLibSubmodule { await Promise.all(promises); if (counts.links) { - new Notice(`${this.plugin.manifest.name}: Updated ${counts.links} links in ${counts.files} files.`) + new Notice(`${this.plugin.manifest.name}: Updated ${counts.links} links in ${counts.files} files.`); } return newFile; diff --git a/src/lib/copy-link.ts b/src/lib/copy-link.ts index 127cb221..40730005 100644 --- a/src/lib/copy-link.ts +++ b/src/lib/copy-link.ts @@ -57,7 +57,7 @@ export class copyLinkLib extends PDFPlusLibSubmodule { }; } } - return null + return null; } getTemplateVariables(subpathParams: Record) { @@ -292,7 +292,7 @@ export class copyLinkLib extends PDFPlusLibSubmodule { } } } - })() + })(); } return true; @@ -341,7 +341,7 @@ export class copyLinkLib extends PDFPlusLibSubmodule { copyLinkToAnnotationWithGivenTextAndFile(text: string, file: TFile, child: PDFViewerChild, checking: boolean, templates: { copyFormat: string, displayTextFormat?: string }, page: number, id: string, colorName: string, autoPaste?: boolean) { if (!checking) { (async () => { - const evaluated = this.getTextToCopy(child, templates.copyFormat, templates.displayTextFormat, file, page, `#page=${page}&annotation=${id}`, text, colorName) + const evaluated = this.getTextToCopy(child, templates.copyFormat, templates.displayTextFormat, file, page, `#page=${page}&annotation=${id}`, text, colorName); await navigator.clipboard.writeText(evaluated); this.onCopyFinish(evaluated); @@ -387,14 +387,14 @@ export class copyLinkLib extends PDFPlusLibSubmodule { // TODO: Needs refactor if (rects) { - const left = Math.min(...rects.map((rect) => rect[0])) - const top = Math.max(...rects.map((rect) => rect[3])) + const left = Math.min(...rects.map((rect) => rect[0])); + const top = Math.max(...rects.map((rect) => rect[3])); if (typeof left === 'number' && typeof top === 'number') { this.plugin.lastCopiedDestInfo = { file, destArray: [page - 1, 'XYZ', left, top, null] }; } } }, 300); - }) + }); } return true; @@ -736,7 +736,7 @@ export class copyLinkLib extends PDFPlusLibSubmodule { editor.replaceSelection(text); } else { let data = editor.getValue(); - data = data.trimEnd() + data = data.trimEnd(); if (data) data += '\n\n'; data += text; editor.setValue(data); @@ -755,7 +755,7 @@ export class copyLinkLib extends PDFPlusLibSubmodule { // Otherwise we just use the vault interface await this.app.vault.process(file, (data) => { // If the file does not end with a blank line, add one - data = data.trimEnd() + data = data.trimEnd(); if (data) data += '\n\n'; data += text; return data; diff --git a/src/lib/dummy-file-manager.ts b/src/lib/dummy-file-manager.ts new file mode 100644 index 00000000..57443e87 --- /dev/null +++ b/src/lib/dummy-file-manager.ts @@ -0,0 +1,134 @@ +import { Editor, MarkdownFileInfo, MarkdownView, normalizePath, Notice, ObsidianProtocolData, Platform, TFile } from 'obsidian'; + +import { PDFPlusLibSubmodule } from './submodule'; +import { DummyFileModal } from 'modals'; +import { matchModifiers } from 'utils'; + + +export class DummyFileManager extends PDFPlusLibSubmodule { + async createDummyFilesInFolder(folderPath: string, uris: string[]) { + // Create the parent folder if it doesn't exist + const folderExists = !!(this.app.vault.getFolderByPath(folderPath)); + if (!folderExists) { + try { + await this.app.vault.createFolder(folderPath); + } catch (error) { + console.error(`${this.plugin.manifest.name}: Failed to create folder "${folderPath}" due to the following error: `, error); + return []; + } + } + + return await Promise.all(uris.map(async (uri) => { + // Find an available file path in the folder + let fileName = normalizePath(folderPath + '/' + uri.split('/').pop()!.replace(/%20/g, ' ')); + if (fileName.endsWith('.pdf')) { + fileName = fileName.slice(0, -4); + } + const availableFilePath = this.app.vault.getAvailablePath(fileName, 'pdf'); + // Create the dummy file + try { + const file = await this.app.vault.create(availableFilePath, uri); + return file; + } catch (error) { + console.error(`${this.plugin.manifest.name}: Failed to create a dummy file "${availableFilePath}" due to the following error: `, error); + throw error; + } + })); + } + + async createDummyFilesFromObsidianUrl(params: ObsidianProtocolData) { + // Ignore everything before https://, http:// or file:///, e.g. "chrome-extension://..." + const url = params['create-dummy'].replace(/^.*((https?)|(file):\/\/)/, '$1'); + const modal = new DummyFileModal(this.plugin); + modal.source = url.startsWith('http') ? 'web' : 'file'; + modal.uris = [url]; + + if ('folder' in params) { + const folderPath = params.folder; + modal.folderPath = normalizePath(folderPath); + await modal.createDummyFiles(); + + // If the folder path is provided, a dummy file named "Untitled.pdf" is created. + // The user will want to rename it, so we focus on the title bar so that the user can start typing right away. + const view = this.lib.workspace.getActivePDFView(); + if (view) { + view.setEphemeralState({ rename: 'all' }); + } + + return; + } + + // If the folder path is not provided, ask the user for it + modal.open(); + } + + async createDummyFilesOnEditorDrop(evt: DragEvent, editor: Editor, info: MarkdownView | MarkdownFileInfo) { + // Check if this event has been already handled by another plugin + if (evt.defaultPrevented) return; + + if (!matchModifiers(evt, this.settings.modifierToDropExternalPDFToCreateDummy)) return; + if (!evt.dataTransfer) return; + + const uris = this.getUrisFromDataTransfer(evt.dataTransfer); + + if (uris.length) { + evt.preventDefault(); + const folderPath = this.getFolderPathForDummyFiles(info.file); + const dummyFiles = await this.createDummyFilesInFolder(folderPath, uris); + + new Notice(`${this.plugin.manifest.name}: Created dummy files created successfully.`); + + // Insert links to dummy files into the editor + dummyFiles.forEach((dummyFile, index) => { + let text = this.app.fileManager.generateMarkdownLink(dummyFile, info.file?.path ?? ''); + if (index < dummyFiles.length - 1) { + text += '\n\n'; + } + editor.replaceSelection(text); + }); + } + } + + getUrisFromDataTransfer(dataTransfer: DataTransfer) { + if (Platform.isDesktopApp) { // file.path is available only in the desktop app + const droppedFiles = Array.from(dataTransfer.files); + if (droppedFiles.length && droppedFiles.every((file) => file.type === 'application/pdf')) { + // Now, `files` is ensured to contain only PDF files + return droppedFiles.map((file) => this.absolutePathToFileUri(file.path)); + } + } + + const draggedUris = dataTransfer.getData('text/uri-list') + .split('\r\n') + .filter((uri) => !uri.startsWith('#')); + + if (draggedUris.length && draggedUris.every((uri) => this.isUriPdf(uri))) { + return draggedUris; + } + + return []; + } + + getFolderPathForDummyFiles(sourceFile: TFile | null) { + const value = this.settings.dummyFileFolderPath + // An empty string means fallback to Obsidian's attachment folder + || this.app.vault.getConfig('attachmentFolderPath'); + + if (value === '.' || value.startsWith('./')) { + return normalizePath((sourceFile?.parent ?? this.app.vault.getRoot()).path + '/' + value.slice(1)); + } + + return normalizePath(value); + } + + absolutePathToFileUri(absPath: string) { + absPath = absPath.replace(/\\/g, '/').replace(/ /g, '%20'); + return 'file://' + (absPath.startsWith('/') ? '' : '/') + absPath; + } + + isUriPdf(uri: string) { + return this.settings.externalURIPatterns + .map((pattern) => new RegExp(pattern)) + .some((re) => re.test(uri)); + } +} diff --git a/src/lib/highlights/write-file/index.ts b/src/lib/highlights/write-file/index.ts index f53f7fa7..ec4b9c68 100644 --- a/src/lib/highlights/write-file/index.ts +++ b/src/lib/highlights/write-file/index.ts @@ -51,7 +51,7 @@ export class AnnotationWriteFileLib extends PDFPlusLibSubmodule { file: child.file, page, ...await this.addAnnotationToTextRange(annotator, child, page, beginIndex, beginOffset, endIndex, endOffset) - } + }; } return null; } diff --git a/src/lib/index.ts b/src/lib/index.ts index 13d93f03..e9104495 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -19,11 +19,12 @@ import { PDFCroppedEmbed } from 'pdf-cropped-embed'; import { PDFBacklinkIndex } from './pdf-backlink-index'; import { Speech } from './speech'; import * as utils from 'utils'; +import { DummyFileManager } from './dummy-file-manager'; export class PDFPlusLib { app: App; - plugin: PDFPlus + plugin: PDFPlus; PDFOutlines = PDFOutlines; NameTree = NameTree; @@ -37,6 +38,7 @@ export class PDFPlusLib { highlight: HighlightLib; workspace: WorkspaceLib; composer: PDFComposer; + dummyFileManager: DummyFileManager; speech: Speech; utils = utils; @@ -50,6 +52,7 @@ export class PDFPlusLib { this.highlight = new HighlightLib(plugin); this.workspace = new WorkspaceLib(plugin); this.composer = new PDFComposer(plugin); + this.dummyFileManager = new DummyFileManager(plugin); this.speech = new Speech(plugin); } @@ -206,7 +209,7 @@ export class PDFPlusLib { getColorPaletteAssociatedWithNode(node: Node) { const toolbarEl = this.getToolbarAssociatedWithNode(node); if (!toolbarEl) return null; - const paletteEl = toolbarEl.querySelector('.' + ColorPalette.CLS) + const paletteEl = toolbarEl.querySelector('.' + ColorPalette.CLS); if (!paletteEl) return null; return ColorPalette.elInstanceMap.get(paletteEl) ?? null; @@ -255,7 +258,7 @@ export class PDFPlusLib { if (!child && c.containerEl.contains(node)) { child = c; } - }) + }); } return child ?? null; @@ -306,7 +309,7 @@ export class PDFPlusLib { ...dest .slice(2) as (number | null)[] // .filter((param: number | null): param is number => typeof param === 'number') - ] + ]; } normalizePdfLibDestArray(dest: PDFArray, doc: PDFDocument): DestArray | null { @@ -413,7 +416,7 @@ export class PDFPlusLib { return { page: annot.parent.page.pageNumber, id: annot.data.id, - } + }; } getAnnotationInfoFromPopupEl(popupEl: HTMLElement) { @@ -452,8 +455,12 @@ export class PDFPlusLib { } /** - * The same as app.fileManager.generateMarkdownLink(), but respects the "alias" parameter for non-markdown files as well. - * See https://github.com/obsidianmd/obsidian-api/issues/154 + * The same as `app.fileManager.generateMarkdownLink()`, but before Obsidian 1.7, it did not respect + * the `alias` parameter for non-markdown files. + * This function fixes that issue. Other than that, it is the same as the original function. + * + * Note that this problem has been fixed in Obsidian 1.7. However, it will make sense to keep using this + * function for make this plugin work for older versions of Obsidian without an extra `requireApiVersion` check. */ generateMarkdownLink(file: TFile, sourcePath: string, subpath?: string, alias?: string) { const app = this.app; @@ -791,7 +798,7 @@ export class PDFPlusLib { let view: PDFView | null = null; this.workspace.iteratePDFViews((v) => { if (v.viewer.child === child) view = v; - }) + }); return view; } @@ -817,7 +824,7 @@ export class PDFPlusLib { isCanvasView(view: View): view is CanvasView { // The instanceof check is necessary for correctly handling DeferredView. - return view instanceof TextFileView && view.getViewType() === 'canvas' && 'canvas' in view + return view instanceof TextFileView && view.getViewType() === 'canvas' && 'canvas' in view; } isCanvasPDFNode(node: CanvasNode): node is CanvasFileNode { @@ -837,11 +844,13 @@ export class PDFPlusLib { } getAvailablePathForCopy(file: TFile) { - return this.app.vault.getAvailablePath(removeExtension(file.path), file.extension) + return this.app.vault.getAvailablePath(removeExtension(file.path), file.extension); } + + get metadataCacheUpdatePromise() { - return new Promise((resolve) => this.app.metadataCache.onCleanCache(resolve)) + return new Promise((resolve) => this.app.metadataCache.onCleanCache(resolve)); } async renderPDFPageToCanvas(page: PDFPageProxy, resolution?: number): Promise { diff --git a/src/lib/name-or-number-trees.ts b/src/lib/name-or-number-trees.ts index ec03e304..99abf3d9 100644 --- a/src/lib/name-or-number-trees.ts +++ b/src/lib/name-or-number-trees.ts @@ -174,7 +174,7 @@ abstract class NameOrNumberTreeNode { return { done: false, value: [key, value] }; } - } + }; } keys() { @@ -276,7 +276,7 @@ abstract class NameOrNumberTreeNode { const newLeaf = this.dict.context.obj({ [this.leafKey]: namesOrNums, Limits: limits }); const newLeafRef = this.dict.context.register(newLeaf); newLeafRefs.push(newLeafRef); - } + }; for (const [key, value] of this) { keyVals.push(key, value); @@ -340,7 +340,7 @@ abstract class NameOrNumberTreeNode { const keyOrVals = this.dict.get(PDFName.of(this.leafKey)); if (!(keyOrVals instanceof PDFArray)) return null; return keyOrVals.asArray().map((keyOrVal, index) => { - return index % 2 ? keyOrVal : this._toStringOrNumber(keyOrVal) + return index % 2 ? keyOrVal : this._toStringOrNumber(keyOrVal); }); } } diff --git a/src/lib/outlines.ts b/src/lib/outlines.ts index 6cbb9072..67690b23 100644 --- a/src/lib/outlines.ts +++ b/src/lib/outlines.ts @@ -127,7 +127,7 @@ export class PDFOutlines { str = str + ' '.repeat(item.depth - 1) + '- ' + item.title + '\n'; } } - }) + }); return str; } diff --git a/src/lib/pdf-backlink-index.ts b/src/lib/pdf-backlink-index.ts index 86e028da..c99b2d0c 100644 --- a/src/lib/pdf-backlink-index.ts +++ b/src/lib/pdf-backlink-index.ts @@ -112,7 +112,7 @@ export class PDFBacklinkIndex extends PDFPlusComponent { cache.page = pageNumber; if (params.has('selection')) { - const selectionPos = params.get('selection')!.split(',').map((s) => parseInt(s.trim())) + const selectionPos = params.get('selection')!.split(',').map((s) => parseInt(s.trim())); if (selectionPos.length === 4 && selectionPos.every((pos) => !isNaN(pos))) { const [beginIndex, beginOffset, endIndex, endOffset] = selectionPos; cache.selection = { beginIndex, beginOffset, endIndex, endOffset }; @@ -340,7 +340,7 @@ export class PDFBacklinkCache { } set selection(selection: PDFBacklinkCache['_selection']) { - const pageIndex = this.getPageIndex() + const pageIndex = this.getPageIndex(); if (pageIndex) { if (this.selection) { diff --git a/src/lib/workspace-lib.ts b/src/lib/workspace-lib.ts index 632e42b7..48779418 100644 --- a/src/lib/workspace-lib.ts +++ b/src/lib/workspace-lib.ts @@ -173,7 +173,7 @@ export class WorkspaceLib extends PDFPlusLibSubmodule { getMarkdownLeafInSidebar(sidebarType: SidebarType) { if (this.settings.singleMDLeafInSidebar) { return this.lib.workspace.getExistingMarkdownLeafInSidebar(sidebarType) - ?? this.lib.workspace.getNewLeafInSidebar(sidebarType) + ?? this.lib.workspace.getNewLeafInSidebar(sidebarType); } else { return this.lib.workspace.getNewLeafInSidebar(sidebarType); } @@ -276,7 +276,7 @@ export class WorkspaceLib extends PDFPlusLibSubmodule { return this.app.workspace.createLeafBySplit(leaf, 'horizontal', direction === 'up'); } } - return this.app.workspace.createLeafInParent(this.app.workspace.rootSplit, 0) + return this.app.workspace.createLeafInParent(this.app.workspace.rootSplit, 0); } getLeafInSidebar(sidebarType: SidebarType): WorkspaceLeaf { diff --git a/src/main.ts b/src/main.ts index 6fbf6ec3..934ac8d7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,7 +10,7 @@ import { PDFCroppedEmbed } from 'pdf-cropped-embed'; import { DEFAULT_SETTINGS, PDFPlusSettings, PDFPlusSettingTab } from 'settings'; import { subpathToParams, OverloadParameters, focusObsidian, isTargetHTMLElement } from 'utils'; import { DestArray, ObsidianViewer, PDFEmbed, PDFView, PDFViewerChild, PDFViewerComponent, Rect } from 'typings'; -import { ExternalPDFModal, InstallerVersionModal } from 'modals'; +import { InstallerVersionModal } from 'modals'; import { PDFExternalLinkPostProcessor, PDFInternalLinkPostProcessor, PDFOutlineItemPostProcessor, PDFThumbnailItemPostProcessor } from 'post-process'; import { BibliographyManager } from 'bib'; @@ -88,7 +88,6 @@ export default class PDFPlus extends Plugin { await this.loadSettings(); await this.saveSettings(); - this.domManager = this.addChild(new DomManager(this)); this.domManager.registerCalloutRenderer(); @@ -153,13 +152,18 @@ export default class PDFPlus extends Plugin { addIcon('vim', 'VIM'); } + getDefaultSettings() { + // Use structuredClone to ensure DEFAULT_SETTINGS and its properties are intact + return structuredClone(DEFAULT_SETTINGS); + } + async restoreDefaultSettings() { - this.settings = structuredClone(DEFAULT_SETTINGS); + this.settings = this.getDefaultSettings(); await this.saveSettings(); } async loadSettings() { - this.settings = Object.assign(structuredClone(DEFAULT_SETTINGS), await this.loadData()); + this.settings = Object.assign(this.getDefaultSettings(), await this.loadData()); // The AnyStyle path had been saved in data.json until v0.39.3, but now it's saved in the local storage if (!this.settings.anystylePath) { @@ -253,7 +257,7 @@ export default class PDFPlus extends Plugin { } private loadContextMenuConfig() { - const defaultConfig = DEFAULT_SETTINGS.contextMenuConfig; + const defaultConfig = this.getDefaultSettings().contextMenuConfig; const config: typeof defaultConfig = []; for (const defaultSectionConfig of defaultConfig) { const existingSectionConfig = this.settings.contextMenuConfig.find(({ id }) => id === defaultSectionConfig.id); @@ -322,7 +326,7 @@ export default class PDFPlus extends Plugin { this.openSettingTab().scrollToHeading('auto-focus'); }); }); - menu.onHide(() => { menuShown = false }); + menu.onHide(() => { menuShown = false; }); menu.showAtMouseEvent(evt); menuShown = true; }); @@ -346,7 +350,7 @@ export default class PDFPlus extends Plugin { this.openSettingTab().scrollToHeading('auto-paste'); }); }); - menu.onHide(() => { menuShown = false }); + menu.onHide(() => { menuShown = false; }); menu.showAtMouseEvent(evt); menuShown = true; }); @@ -506,7 +510,7 @@ export default class PDFPlus extends Plugin { // Prevent the click event causing the editor to select the link like an image embed evt.preventDefault(); } - }) + }); } if (params.has('color')) { @@ -590,6 +594,10 @@ export default class PDFPlus extends Plugin { this.registerEvent(this.app.workspace.on('quit', async () => { await this.cleanUpResources(); })); + + // + // https://github.com/RyotaUshio/obsidian-pdf-plus/issues/285 + this.registerEvent(this.app.workspace.on('editor-drop', (evt, editor, info) => this.lib.dummyFileManager.createDummyFilesOnEditorDrop(evt, editor, info))); } registerOneTimeEvent(events: T, ...[evt, callback, ctx]: OverloadParameters) { @@ -672,7 +680,7 @@ export default class PDFPlus extends Plugin { obsidianProtocolHandler(params: ObsidianProtocolData) { if ('create-dummy' in params) { - return ExternalPDFModal.createDummyFilesFromObsidianUrl(this, params); + return this.lib.dummyFileManager.createDummyFilesFromObsidianUrl(params); } if ('setting' in params) { diff --git a/src/modals/annotation-modals.ts b/src/modals/annotation-modals.ts index aa93e653..013cb9ff 100644 --- a/src/modals/annotation-modals.ts +++ b/src/modals/annotation-modals.ts @@ -50,9 +50,9 @@ export class PDFAnnotationEditModal extends PDFAnnotationModal { static forSubtype(subtype: typeof PDFAnnotationEditModal.supportedSubtypes[number], ...args: ConstructorParameters): PDFAnnotationEditModal { if (subtype === 'Link') { - return PDFAnnotationEditModal.forLinkAnnotation(...args) + return PDFAnnotationEditModal.forLinkAnnotation(...args); } else { - return PDFAnnotationEditModal.forTextMarkupAnnotation(...args) + return PDFAnnotationEditModal.forTextMarkupAnnotation(...args); } } @@ -134,7 +134,7 @@ export class PDFAnnotationEditModal extends PDFAnnotationModal { { type: 'rgb', rgb }, { linktext: false } ); - }) + }); }); }); } @@ -279,7 +279,7 @@ export class PDFAnnotationEditModal extends PDFAnnotationModal { if (this.plugin.settings.renderMarkdownInStickyNote) { setting.setDesc(`Press ${this.app.hotkeyManager.printHotkeyForCommand('markdown:toggle-preview')} to toggle preview.`); } else { - setting.setDesc('Tip: There is an option called "Render markdown in annotation popups when the annotation has text contents".') + setting.setDesc('Tip: There is an option called "Render markdown in annotation popups when the annotation has text contents".'); } }) .addTextArea((textarea) => { @@ -359,7 +359,7 @@ export class PDFAnnotationEditModal extends PDFAnnotationModal { this.previewEl.setCssStyles({ width: `${this.editorEl.clientWidth}px`, height: `${this.editorEl.clientHeight}px` - }) + }); this.previewEl.empty(); await MarkdownRenderer.render(this.app, this.textarea?.getValue() ?? '', this.previewEl, '', this.component); hookInternalLinkMouseEventHandlers(this.app, this.previewEl, this.file.path); diff --git a/src/modals/dummy-file-modals.ts b/src/modals/dummy-file-modals.ts new file mode 100644 index 00000000..638a376e --- /dev/null +++ b/src/modals/dummy-file-modals.ts @@ -0,0 +1,262 @@ +import { PDFPlusModal } from 'modals'; +import { normalizePath, Notice, Platform, Setting } from 'obsidian'; +import { FuzzyFolderSuggest, getModifierNameInPlatform } from 'utils'; + + +export class DummyFileModal extends PDFPlusModal { + static LOCAL_STORAGE_KEY = 'last-used-dummy-file-source'; + + source: 'file' | 'web' | null = null; + uris: string[] = []; + // where to save the dummy files + folderPath: string | null = null; + + constructor(...args: ConstructorParameters) { + super(...args); + + this.scope.register([], 'Enter', () => { + if (activeDocument.activeElement?.tagName === 'INPUT') { + this.submit(); + } + }); + } + + onOpen() { + super.onOpen(); + + const lastUsedSource = this.plugin.loadLocalStorage(DummyFileModal.LOCAL_STORAGE_KEY); + if (['file', 'web'].includes(lastUsedSource)) { + this.source = lastUsedSource; + } + this.folderPath = this.lib.dummyFileManager.getFolderPathForDummyFiles(this.app.workspace.getActiveFile()); + + this.titleEl.setText(`${this.plugin.manifest.name}: Create dummy file for external PDF`); + this.modalEl.createDiv('', (div) => { + new Setting(div).setDesc(createFragment((el) => { + const keys = this.plugin.settings.modifierToDropExternalPDFToCreateDummy; + el.appendText(`You can also use ${keys.length ? (keys.map(getModifierNameInPlatform).join('+') + ' +') : ''} drag & drop to create dummy files. `); + el.createEl('a', { text: 'Learn more about dummy PDF files', href: 'https://ryotaushio.github.io/obsidian-pdf-plus/external-pdf-files' }); + })); + setTimeout(() => { + this.modalEl.insertBefore(div, this.contentEl); + }); + }); + this.display(); + } + + display() { + if (Platform.isDesktopApp) { + this.displayDesktop(); + } else { + this.displayMobile(); + } + } + + displayDesktop() { + this.contentEl.empty(); + + this.addSourceLocationSetting(); + this.addFolderSetting(); + + if (this.source === 'file') { + this.addLocalFileSetting(); + } else if (this.source === 'web') { + this.addWebFileSetting(); + } + + if (this.source) { + this.addButtons(); + } + } + + displayMobile() { + this.source = 'web'; + this.contentEl.empty(); + this.addFolderSetting(); + this.addWebFileSetting(); + this.addButtons(); + } + + addSetting() { + return new Setting(this.contentEl); + } + + addSourceLocationSetting() { + return this.addSetting() + .setName('Source location') + .setDesc('Where the external PDF is located.') + .addDropdown((dropdown) => { + dropdown + .addOptions({ + 'file': 'On this computer', + 'web': 'Web' + }) + .setValue(this.source ?? '') + .onChange((value: 'file' | 'web') => { + this.source = value; + this.display(); + }); + dropdown.selectEl.tabIndex = this.source ? -1 : 0; + }); + } + + addFolderSetting() { + return this.addSetting() + .setName('Folder to save the dummy files') + .setDesc(createFragment((el) => { + el.appendText('You can specify the default folder in the '); + el.createEl('a', { text: 'settings', href: 'obsidian://pdf-plus?setting=dummyFileFolderPath' }); + el.appendText('.'); + })) + .addText((text) => { + text.inputEl.size = 30; + text.setValue(this.folderPath ?? ''); + new FuzzyFolderSuggest(this.app, text.inputEl) + .onSelect(({ item: folder }) => { + this.folderPath = folder.path; + }); + }); + } + + addLocalFileSetting() { + this.addSetting() + .setName('Absolute path to the PDF') + .setDesc('Type the path in the input box or click the "Browse" button to select the file.') + .addButton((button) => { + button + .setButtonText('Browse') + .setCta() + .onClick(() => { + // @ts-ignore + const paths: string[] | undefined = window.electron?.remote.dialog.showOpenDialogSync({ + properties: ['openFile', 'multiSelections', 'dontAddToRecent'], + filters: [ + { name: 'PDF files', extensions: ['pdf'] } + ] + }); + if (paths && paths.length > 0) { + this.uris = paths.map((path) => this.lib.dummyFileManager.absolutePathToFileUri(path)); + this.display(); + } + }); + }) + .addExtraButton((button) => { + button + .setIcon('plus') + .setTooltip('Add another file') + .onClick(() => { + this.uris.push(''); + this.display(); + }); + }); + + this.addUriListSetting(); + } + + addWebFileSetting() { + this.addSetting() + .setName('URL of the PDF') + .setDesc('Must start with "https://" or "http://".') + .addExtraButton((button) => { + button + .setIcon('plus') + .setTooltip('Add another URL') + .onClick(() => { + this.uris.push(''); + this.display(); + }); + }); + + this.addUriListSetting(); + } + + addUriListSetting() { + if (!this.uris.length) this.uris.push(''); + + for (let i = 0; i < this.uris.length; i++) { + this.addSetting() + .then((setting) => setting.settingEl.addClass('no-border')) + .addText((text) => { + text.inputEl.size = 30; + if (this.source === 'file') { + text.setValue(this.uris[i] ? this.uris[i].replace(/^file:\/\//, '') : '') + .onChange((value) => { + this.uris[i] = 'file://' + value; + }); + } else { + text.setValue(this.uris[i] || '') + .onChange((value) => { + this.uris[i] = value; + }); + } + // auto-focus the last input + if (i === this.uris.length - 1) { + setTimeout(() => text.inputEl.focus()); + } + }) + .addExtraButton((button) => { + button + .setIcon('trash') + .setTooltip(`Remove this ${this.source === 'file' ? 'file' : 'URL'}`) + .onClick(() => { + this.uris.splice(i, 1); + this.display(); + }); + if (this.uris.length === 1) { + button.extraSettingsEl.hide(); + } + }); + } + } + + addButtons() { + this.contentEl.createDiv('modal-button-container', (buttonContainerEl) => { + buttonContainerEl.createEl('button', { text: 'Create', cls: 'mod-cta' }, (buttonEl) => { + buttonEl.addEventListener('click', () => { + this.submit(); + }); + }); + + buttonContainerEl.createEl('button', { text: 'Cancel' }, (buttonEl) => { + buttonEl.addEventListener('click', () => { + this.close(); + }); + }); + }); + } + + submit() { + this.uris = this.uris.filter((uri) => uri); + + if (!this.uris.length) { + new Notice(`${this.plugin.manifest.name}: The external PDF location is not specified.`); + return; + } + if (!this.folderPath) { + new Notice(`${this.plugin.manifest.name}: The folder to save the dummy files is not specified.`); + return; + } + + this.plugin.saveLocalStorage(DummyFileModal.LOCAL_STORAGE_KEY, this.source); + + this.createDummyFiles(); + this.close(); + } + + async createDummyFiles() { + if (this.folderPath) { + this.folderPath = normalizePath(this.folderPath); + const files = await this.lib.dummyFileManager.createDummyFilesInFolder(this.folderPath, this.uris); + new Notice(`${this.plugin.manifest.name}: Dummy files created successfully.`); + + for (const file of files) { + if (file) { + const leaf = this.app.workspace.getLeaf(true); + await leaf.openFile(file); + } + } + } else { + new Notice(`${this.plugin.manifest.name}: Failed to create dummy files for the following URIs: ${this.uris.join(', ')}`); + } + } +} diff --git a/src/modals/external-pdf-modals.ts b/src/modals/external-pdf-modals.ts deleted file mode 100644 index cd7103ee..00000000 --- a/src/modals/external-pdf-modals.ts +++ /dev/null @@ -1,345 +0,0 @@ -import PDFPlus from 'main'; -import { PDFPlusModal } from 'modals'; -import { Notice, ObsidianProtocolData, Platform, Setting, TFile, normalizePath } from 'obsidian'; -import { FuzzyFolderSuggest } from 'utils'; - - -export class ExternalPDFModal extends PDFPlusModal { - source: 'file' | 'web' | null = null; - urls: string[] = []; - // for source = 'file' - folderPath: string | null = null; - // for source = 'web' - filePath: string | null = null; - - constructor(...args: ConstructorParameters) { - super(...args); - - this.scope.register([], 'Enter', () => { - if (activeDocument.activeElement?.tagName === 'INPUT') { - this.submit(); - } - }); - } - - onOpen() { - super.onOpen(); - this.titleEl.setText(`${this.plugin.manifest.name}: Create dummy file for external PDF`); - this.modalEl.createDiv('', (div) => { - new Setting(div).setDesc(createFragment((el) => { - el.createEl('a', { text: 'Learn more', href: 'https://ryotaushio.github.io/obsidian-pdf-plus/external-pdf-files' }); - })); - setTimeout(() => { - this.modalEl.insertBefore(div, this.contentEl); - }); - }); - this.display(); - } - - display() { - if (Platform.isDesktopApp) { - this.displayDesktop(); - } else { - this.displayMobile(); - } - } - - displayDesktop() { - this.contentEl.empty(); - - this.addSourceLocationSetting(); - - if (this.source === 'file') { - this.addLocalFileSetting(); - } else if (this.source === 'web') { - this.addWebFileSetting(); - } - - if (this.source) { - this.addButtons(); - } - } - - displayMobile() { - this.source = 'web'; - this.contentEl.empty(); - this.addWebFileSetting(); - this.addButtons(); - } - - addSetting() { - return new Setting(this.contentEl); - } - - addSourceLocationSetting() { - this.addSetting() - .setName('Source location') - .setDesc('Where the external PDF is located.') - .addDropdown((dropdown) => { - dropdown - .addOptions({ - 'file': 'On this computer', - 'web': 'Web' - }) - .setValue(this.source ?? '') - .onChange((value: 'file' | 'web') => { - this.source = value; - this.display(); - }); - dropdown.selectEl.tabIndex = this.source ? -1 : 0; - }); - } - - addLocalFileSetting() { - this.addSetting() - .setName('Folder to save the dummy files') - .addText((text) => { - text.inputEl.size = 30; - text.setValue(this.folderPath ?? ''); - new FuzzyFolderSuggest(this.app, text.inputEl) - .onSelect(({ item: folder }) => { - this.folderPath = folder.path; - }); - }); - this.addSetting() - .setName('Absolute path to the PDF') - .setDesc('Type the path in the input box or click the "Browse" button to select the file.') - .addButton((button) => { - button - .setButtonText('Browse') - .setCta() - .onClick(() => { - // @ts-ignore - const paths: string[] | undefined = window.electron?.remote.dialog.showOpenDialogSync({ - properties: ['openFile', 'multiSelections', 'dontAddToRecent'], - filters: [ - { name: 'PDF files', extensions: ['pdf'] } - ] - }); - if (paths && paths.length > 0) { - this.urls = paths.map((path) => { - path = path.replace(/\\/g, '/').replace(/ /g, '%20'); - return 'file://' + (path.startsWith('/') ? '' : '/') + path; - }); - this.display(); - } - }); - }) - .addExtraButton((button) => { - button - .setIcon('plus') - .setTooltip('Add another file') - .onClick(() => { - this.urls.push(''); - this.display(); - }); - }); - if (!this.urls.length) this.urls.push(''); - - for (let i = 0; i < this.urls.length; i++) { - this.addSetting() - .then((setting) => setting.settingEl.addClass('no-border')) - .addText((text) => { - text.inputEl.size = 30; - text.setValue(this.urls[i] ? this.urls[i].replace(/^file:\/\//, '') : '') - .onChange((value) => { - this.urls[i] = 'file://' + value; - }); - }) - .addExtraButton((button) => { - button - .setIcon('trash') - .setTooltip('Remove this file') - .onClick(() => { - this.urls.splice(i, 1); - this.display(); - }); - if (this.urls.length === 1) { - button.extraSettingsEl.hide(); - } - }); - } - } - - addWebFileSetting() { - this.addSetting() - .setName('Dummy file path') - .setDesc('Must end with ".pdf".') - .addText((text) => { - text.inputEl.size = 30; - text.setPlaceholder('e.g. Folder/File.pdf') - .setValue(this.filePath ?? '') - new FuzzyFolderSuggest(this.app, text.inputEl, { blurOnSelect: false }) - .onSelect(({ item: folder }) => { - setTimeout(() => { - const path = normalizePath(folder.path + '/Untitled.pdf'); - text.setValue(path); - this.filePath = path; - text.inputEl.setSelectionRange(path.lastIndexOf('/') + 1, path.lastIndexOf('.')); - }); - }) - text.onChange((value) => { - if (!value || value.endsWith('.pdf')) { - this.filePath = value; - text.inputEl.removeClass('error'); - } else { - this.filePath = null; - text.inputEl.addClass('error'); - } - }); - }); - this.addSetting() - .setName('URL of the PDF') - .setDesc('Must start with "https://" or "http://".') - .addText((text) => { - text.inputEl.size = 30; - if (this.urls.length) text.setValue(this.urls[0]); - text.onChange((value) => { - if (!value || value.startsWith('https://') || value.startsWith('http://')) { - this.urls = [value]; - text.inputEl.removeClass('error'); - } else { - this.urls = []; - text.inputEl.addClass('error'); - } - }); - }); - } - - addButtons() { - this.contentEl.createDiv('modal-button-container', (buttonContainerEl) => { - buttonContainerEl.createEl('button', { text: 'Create', cls: 'mod-cta' }, (buttonEl) => { - buttonEl.addEventListener('click', () => { - this.submit(); - }); - }); - - buttonContainerEl.createEl('button', { text: 'Cancel' }, (buttonEl) => { - buttonEl.addEventListener('click', () => { - this.close(); - }); - }); - }); - } - - submit() { - this.urls = this.urls.filter((url) => url); - - if (!this.urls.length) { - new Notice(`${this.plugin.manifest.name}: The external PDF location is not specified.`) - return; - } - if (this.source === 'file' && !this.folderPath) { - new Notice(`${this.plugin.manifest.name}: The folder to save the dummy files is not specified.`) - return; - } - if (this.source === 'web' && !this.filePath) { - new Notice(`${this.plugin.manifest.name}: The dummy file path is not specified.`) - return; - } - - this.createDummyFiles(); - this.close(); - } - - async createDummyFiles() { - let failed: string[] = []; - const promises: Promise[] = []; - - const createDummyFile = async (url: string, filePath: string) => { - // Create the parent folder if it doesn't exist - const folderPath = normalizePath(filePath.split('/').slice(0, -1).join('/')); - if (folderPath) { - const folderExists = !!(this.app.vault.getAbstractFileByPath(folderPath)); - if (!folderExists) { - await this.app.vault.createFolder(folderPath); - } - } - - // Find an available file path in that folder - const availableFilePath = this.app.vault.getAvailablePath(filePath.slice(0, -4), 'pdf') - - // Create the dummy file - const file = await this.app.vault.create(availableFilePath, url); - return file; - }; - - if (this.source === 'file' && this.folderPath) { - for (const url of this.urls) { - const filePath = normalizePath(this.folderPath + '/' + url.split('/').pop()?.replace(/%20/g, ' ') ?? ''); - if (!filePath.endsWith('.pdf')) { - failed.push(url); - continue; - } - - promises.push( - createDummyFile(url, filePath).catch((err) => { - failed.push(url); - console.error(err); - return null; - }) - ); - } - } else if (this.source === 'web' && this.filePath && this.urls.length) { - const filePath = normalizePath(this.filePath); - if (!filePath.endsWith('.pdf')) { - failed = this.urls; - } else { - promises.push( - createDummyFile(this.urls[0], filePath).catch((err) => { - failed = this.urls; - console.error(err); - return null; - }) - ); - } - } else { - failed = this.urls; - } - - const files = await Promise.all(promises); - - if (failed.length) { - new Notice(`${this.plugin.manifest.name}: Failed to create dummy files for the following URLs: ${failed.join(', ')}`); - } else { - new Notice(`${this.plugin.manifest.name}: Dummy files created successfully.`); - } - - for (const file of files) { - if (file) { - const leaf = this.app.workspace.getLeaf(true); - await leaf.openFile(file); - } - } - } - - static async createDummyFilesFromObsidianUrl(plugin: PDFPlus, params: ObsidianProtocolData) { - // Ignore everything before https://, http:// or file:///, e.g. "chrome-extension://..." - const url = params['create-dummy'].replace(/^.*((https?)|(file):\/\/)/, '$1'); - const modal = new ExternalPDFModal(plugin); - modal.source = url.startsWith('http') ? 'web' : 'file'; - modal.urls = [url]; - - if ('folder' in params) { - const folderPath = params.folder; - if (modal.source === 'web') { - modal.filePath = normalizePath(folderPath + '/Untitled.pdf'); - } else { - modal.folderPath = normalizePath(folderPath); - } - await modal.createDummyFiles(); - - // If the folder path is provided, a dummy file named "Untitled.pdf" is created. - // The user will want to rename it, so we focus on the title bar so that the user can start typing right away. - const view = plugin.lib.workspace.getActivePDFView(); - if (view) { - view.setEphemeralState({ rename: 'all' }); - } - - return; - } - - // If the folder path is not provided, ask the user for it - modal.open(); - } -} diff --git a/src/modals/index.ts b/src/modals/index.ts index 5ced67b7..d5bb634d 100644 --- a/src/modals/index.ts +++ b/src/modals/index.ts @@ -3,5 +3,5 @@ export * from './annotation-modals'; export * from './pdf-composer-modals'; export * from './outline-modals'; export * from './page-label-modals'; -export * from './external-pdf-modals'; +export * from './dummy-file-modals'; export * from './installer-version-modal'; diff --git a/src/modals/outline-modals.ts b/src/modals/outline-modals.ts index 1d2f4cc5..f210fc7e 100644 --- a/src/modals/outline-modals.ts +++ b/src/modals/outline-modals.ts @@ -117,7 +117,7 @@ export class PDFOutlineMoveModal extends FuzzySuggestModal { } askDestination() { - this.open() + this.open(); return this; } diff --git a/src/modals/page-label-modals.ts b/src/modals/page-label-modals.ts index 1e29e404..9c47557a 100644 --- a/src/modals/page-label-modals.ts +++ b/src/modals/page-label-modals.ts @@ -62,7 +62,7 @@ class PDFPageLabelSettingsForRange { if (isPageLabelNumberingStyle(value)) this.dict.style = value; else delete this.dict.style; }); - }) + }); } addStartSetting() { @@ -118,7 +118,7 @@ export class PDFPageLabelEditModal extends PDFPageLabelModal { setting.descEl, '', this.component - ) + ); }) .then((setting) => this.contentEl.prepend(setting.settingEl)); @@ -169,7 +169,7 @@ export class PDFPageLabelEditModal extends PDFPageLabelModal { const pageCount = doc.getPageCount(); for (let i = 0; i < pageLabels.ranges.length; i++) { - const rangeEl = this.controlEl.createDiv('page-label-range') + const rangeEl = this.controlEl.createDiv('page-label-range'); const range = pageLabels.ranges[i]; const prevRange = pageLabels.ranges[i - 1]; @@ -187,7 +187,7 @@ export class PDFPageLabelEditModal extends PDFPageLabelModal { pageLabels.divideRangeAtPage(range.pageFrom + 1, false); this.redisplay(); }); - }) + }); } }) .addExtraButton((button) => { @@ -245,7 +245,7 @@ export class PDFPageLabelEditModal extends PDFPageLabelModal { if (text.disabled) { setTooltip(text.inputEl, 'The last range cannot be extended.'); } - }) + }); text.inputEl.addEventListener('blur', () => this.redisplay(), { once: true }); }) .then((setting) => this.addPreviewButton(setting, pageTo)); @@ -260,7 +260,7 @@ export class PDFPageLabelEditModal extends PDFPageLabelModal { .setHeading() .then((setting) => { const iconEl = createDiv(); - setting.settingEl.prepend(iconEl) + setting.settingEl.prepend(iconEl); setIcon(iconEl, iconName); }); } @@ -277,9 +277,9 @@ export class PDFPageLabelEditModal extends PDFPageLabelModal { linktext: this.file.path + `#page=${page}`, targetEl: button.extraSettingsEl, hoverParent: this.component - }) + }); }); - }) + }); }); } @@ -293,7 +293,7 @@ export class PDFPageLabelEditModal extends PDFPageLabelModal { .onClick(async () => { if (this.pageLabels && this.doc) { if (this.pageLabels.rangeCount() > 0) { - this.pageLabels.setToDocument(this.doc) + this.pageLabels.setToDocument(this.doc); } else PDFPageLabels.removeFromDocument(this.doc); await this.app.vault.modifyBinary(this.file, await this.doc.save()); } else { diff --git a/src/modals/pdf-composer-modals.ts b/src/modals/pdf-composer-modals.ts index 1684ddd3..25989162 100644 --- a/src/modals/pdf-composer-modals.ts +++ b/src/modals/pdf-composer-modals.ts @@ -51,7 +51,7 @@ export class PDFPageDeleteModal extends PDFPlusModal { .setButtonText('Cancel') .onClick(() => { this.#resolve(false); - this.close() + this.close(); }); }) .then((setting) => setting.setClass('no-border')); @@ -114,7 +114,7 @@ export class PDFComposerModal extends PDFPlusModal { if (this.askPageLabelUpdateMethod || this.askInPlace) this.open(); else this.#resolve({ pageLabelUpdateMethod: this.defaultMethod, inPlace: this.defaultInPlace }); - return this + return this; } then(callback: (keepLabels: boolean, inPlace: boolean) => any) { @@ -138,7 +138,7 @@ export class PDFComposerModal extends PDFPlusModal { new Setting(this.contentEl) .setName('Update the page labels?') .setDesc(createFragment((el) => { - el.createEl('a', { text: 'Learn more', href: 'https://github.com/RyotaUshio/obsidian-pdf-plus/wiki/Page-labels' }) + el.createEl('a', { text: 'Learn more', href: 'https://github.com/RyotaUshio/obsidian-pdf-plus/wiki/Page-labels' }); })) .addDropdown((dropdown) => { dropdown diff --git a/src/modals/restore-default-modal.ts b/src/modals/restore-default-modal.ts new file mode 100644 index 00000000..2e8a8df9 --- /dev/null +++ b/src/modals/restore-default-modal.ts @@ -0,0 +1,43 @@ +import { ButtonComponent, Notice } from 'obsidian'; + +import { PDFPlusModal } from './base-modal'; + + +export class RestoreDefaultModal extends PDFPlusModal { + onOpen(): void { + super.onOpen(); + this.containerEl.addClass('pdf-plus-restore-default-modal'); + this.titleEl.setText(`${this.plugin.manifest.name}: Restore default settings`); + this.contentEl.createEl('p', { + text: `This operation will overwrite your PDF++ config file (${( + this.plugin.manifest.dir + ?? (this.app.vault.configDir + '/plugins/' + this.plugin.manifest.id) + ) + '/data.json' + }). You may want to back up the file before proceeding.`, + }); + + this.contentEl.createDiv('modal-button-container', (el) => { + new ButtonComponent(el) + .setButtonText('I understand, restore default settings') + .setWarning() + .onClick(async () => { + await this.plugin.restoreDefaultSettings(); + this.close(); + new Notice(`${this.plugin.manifest.name}: Default setting restored. Note that some options require a restart to take effect.`, 6000); + }); + new ButtonComponent(el) + .setButtonText('Cancel') + .onClick(() => { + this.close(); + }); + }); + + setTimeout(() => { + const activeEl = this.containerEl.doc.activeElement; + if (activeEl && activeEl.instanceOf(HTMLButtonElement) && this.containerEl.contains(activeEl)) { + // Avoid an accidental press of the button + activeEl.blur(); + } + }); + } +} diff --git a/src/patchers/backlink.ts b/src/patchers/backlink.ts index efbcb62d..2dfa69a4 100644 --- a/src/patchers/backlink.ts +++ b/src/patchers/backlink.ts @@ -36,7 +36,7 @@ export const patchBacklink = (plugin: PDFPlus): boolean => { if (this.getViewType() === 'backlink' && file.extension === 'pdf') { this.pdfManager = new BacklinkPanePDFManager(plugin, this.backlink, file).setParents(plugin, this); } - } + }; }, onUnloadFile(old) { return async function (file: TFile) { @@ -45,7 +45,7 @@ export const patchBacklink = (plugin: PDFPlus): boolean => { self.pdfManager.unload(); } await old.call(this, file); - } + }; } })); @@ -79,7 +79,7 @@ export const patchBacklink = (plugin: PDFPlus): boolean => { } return old.call(this, file, result, content, showTitle); - } + }; } })); diff --git a/src/patchers/clipboard-manager.ts b/src/patchers/clipboard-manager.ts index 9e50dee6..5a4a104c 100644 --- a/src/patchers/clipboard-manager.ts +++ b/src/patchers/clipboard-manager.ts @@ -42,7 +42,7 @@ export const patchClipboardManager = (plugin: PDFPlus) => { setDragEffect(evt, 'link'); app.dragManager.setAction('Insert link here'); } - } + }; }, handleDrop(old) { return function (this: ClipboardManager, evt: DragEvent): boolean | undefined { @@ -79,7 +79,7 @@ export const patchClipboardManager = (plugin: PDFPlus) => { } return false; - } + }; } })); @@ -108,6 +108,6 @@ function setDragEffect(evt: DragEvent, dropEffect: DropEffect) { return evt.dataTransfer.dropEffect = dropEffect; const allowDropAffects = allowDropEffectMap[evt.dataTransfer.effectAllowed]; if (allowDropAffects.contains(dropEffect)) { - evt.dataTransfer.dropEffect = dropEffect + evt.dataTransfer.dropEffect = dropEffect; } } diff --git a/src/patchers/menu.ts b/src/patchers/menu.ts index f89af7fb..d3262859 100644 --- a/src/patchers/menu.ts +++ b/src/patchers/menu.ts @@ -13,13 +13,13 @@ export const patchMenu = (plugin: PDFPlus) => { } plugin.shownMenus.add(this); return old.call(this, ...args); - } + }; }, hide(old) { return function (this: Menu, ...args: any[]) { plugin.shownMenus.delete(this); return old.call(this, ...args); - } + }; } })); -} +}; diff --git a/src/patchers/page-preview.ts b/src/patchers/page-preview.ts index 6b09a02b..48c694ff 100644 --- a/src/patchers/page-preview.ts +++ b/src/patchers/page-preview.ts @@ -43,11 +43,11 @@ export const patchPagePreview = (plugin: PDFPlus): boolean => { } old.call(this, hoverParent, targetEl, linktext, sourcePath, state); - } + }; } })); plugin.patchStatus.pagePreview = true; return true; -} +}; diff --git a/src/patchers/pdf-embed.ts b/src/patchers/pdf-embed.ts index 040e8b09..0ba34de7 100644 --- a/src/patchers/pdf-embed.ts +++ b/src/patchers/pdf-embed.ts @@ -13,4 +13,4 @@ export const patchPDFInternalFromPDFEmbed = (plugin: PDFPlus): boolean => { // don't return true here; if the patch is successful, plugin.patchStatus.pdfInternals // will be set to true when this function is called next time return false; -} +}; diff --git a/src/patchers/pdf-internals.ts b/src/patchers/pdf-internals.ts index 2ad113bb..194fec96 100644 --- a/src/patchers/pdf-internals.ts +++ b/src/patchers/pdf-internals.ts @@ -53,7 +53,7 @@ export const patchPDFInternals = async (plugin: PDFPlus, pdfViewerComponent: PDF return resolve(true); }); }); -} +}; function onPDFInternalsPatchSuccess(plugin: PDFPlus) { const { lib } = plugin; @@ -65,7 +65,7 @@ function onPDFInternalsPatchSuccess(plugin: PDFPlus) { // Clean up the old keymaps already registered by PDFViewerChild, // which causes an error because the listener references the old instance of PDFFindBar. // This keymap hanldler will be re-registered in `PDFViewerChild.load` by the following `viewer.load()`. - const oldEscapeHandler = viewer.scope.keys.find((handler) => handler.modifiers === '' && handler.key === 'Escape') + const oldEscapeHandler = viewer.scope.keys.find((handler) => handler.modifiers === '' && handler.key === 'Escape'); if (oldEscapeHandler) viewer.scope.unregister(oldEscapeHandler); viewer.load(); @@ -88,17 +88,17 @@ const patchPDFViewerComponent = (plugin: PDFPlus, pdfViewerComponent: PDFViewerC }); return ret; - } + }; }, onload(old) { return async function (this: PDFViewerComponent) { const ret = await old.call(this); VimBindings.register(plugin, this); return ret; - } + }; } })); -} +}; const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { const { app, lib } = plugin; @@ -125,7 +125,7 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { let isModEvent = false; const updateIsModEvent = (evt: MouseEvent) => { isModEvent ||= isModifierName(plugin.settings.showContextMenuOnMouseUpIf) && Keymap.isModifier(evt, plugin.settings.showContextMenuOnMouseUpIf); - } + }; this.component.registerDomEvent(viewerContainerEl, 'pointerdown', (evt) => { lib.highlight.viewer.clearRectHighlight(this); @@ -184,7 +184,7 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { new Notice(`${plugin.manifest.name}: An error occurred while mounting the color palette to the toolbar.`); console.error(e); } - } + }; addColorPaletteToToolbar(); plugin.on('update-dom', addColorPaletteToToolbar); @@ -215,14 +215,14 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { } return ret; - } + }; }, unload(old) { return function (this: PDFViewerChild) { this.component?.unload(); return old.call(this); - } + }; }, onResize(old) { return function (this: PDFViewerChild) { @@ -232,7 +232,7 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { } return old.call(this); - } + }; }, loadFile(old) { return async function (this: PDFViewerChild, file: TFile, subpath?: string) { @@ -407,7 +407,7 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { }, plugin.settings.viewSyncPageDebounceInterval * 1000) ); } - } + }; }, /** * Modified applySubpath() from Obsidian's app.js so that @@ -422,22 +422,22 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { const _parseInt = (num: string) => { if (!num) return null; const parsed = parseInt(num); - return Number.isNaN(parsed) ? null : parsed + return Number.isNaN(parsed) ? null : parsed; }; const _parseFloat = (num: string) => { if (!num) return null; const parsed = parseFloat(num); - return Number.isNaN(parsed) ? null : parsed + return Number.isNaN(parsed) ? null : parsed; }; if (subpath) { - subpath = subpath.startsWith('#') ? subpath.substring(1) : subpath + subpath = subpath.startsWith('#') ? subpath.substring(1) : subpath; const pdfViewer = this.pdfViewer; const params = new URLSearchParams(subpath); if (params.has('search') && this.findBar) { - const query = params.get('search')! + const query = params.get('search')!; const settings: Partial = {}; if (plugin.settings.searchLinkHighlightAll !== 'default') { @@ -461,7 +461,7 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { settings[key] = value === 'true'; } } - } + }; parseSearchSettings('highlightAll'); parseSearchSettings('caseSensitive'); @@ -553,19 +553,19 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { dest: JSON.stringify(dest), highlight, // height - } + }; })(subpath); const pdfLoadingTask = pdfViewer.pdfLoadingTask; if (pdfLoadingTask) { - pdfLoadingTask.promise.then(() => pdfViewer.applySubpath(dest)) + pdfLoadingTask.promise.then(() => pdfViewer.applySubpath(dest)); } else { pdfViewer.subpath = dest; } this.subpathHighlight = highlight; } - } + }; }, getMarkdownLink(old) { return function (this: PDFViewerChild, subpath?: string, alias?: string, embed?: boolean): string { @@ -573,7 +573,7 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { const embedLink = lib.generateMarkdownLink(this.file, '', subpath, alias); if (embed) return embedLink; return embedLink.slice(1); - } + }; }, getPageLinkAlias(old) { return function (this: PDFViewerChild, page: number): string { @@ -583,7 +583,7 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { } return old.call(this, page); - } + }; }, highlightText(old) { return function (this: PDFViewerChild, page: number, range: [[number, number], [number, number]]) { @@ -620,7 +620,7 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { }, true); plugin.trigger('highlight', { type: 'selection', source: 'obsidian', pageNumber: page, child: this }); - } + }; }, highlightAnnotation(old) { return function (this: PDFViewerChild, page: number, id: string) { @@ -628,7 +628,7 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { if (this.annotationHighlight) return this.annotationHighlight; const pageView = this.getPage(page); return pageView.annotationLayer?.div.querySelector(`[data-annotation-id="${id}"]`); - } + }; if (plugin.settings.trimSelectionEmbed && this.pdfViewer.isEmbed @@ -657,12 +657,12 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { activeWindow.setTimeout(() => { window.pdfjsViewer.scrollIntoView(el, { top: - plugin.settings.embedMargin - }, true) + }, true); }); } plugin.trigger('highlight', { type: 'annotation', source: 'obsidian', pageNumber: page, child: this }); - } + }; }, clearTextHighlight(old) { return function (this: PDFViewerChild) { @@ -670,7 +670,7 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { return; } old.call(this); - } + }; }, clearAnnotationHighlight(old) { return function (this: PDFViewerChild) { @@ -678,13 +678,13 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { return; } old.call(this); - } + }; }, clearEphemeralUI(old) { return function (this: PDFViewerChild) { old.call(this); lib.highlight.viewer.clearRectHighlight(this); - } + }; }, renderAnnotationPopup(old) { return function (this: PDFViewerChild, annotationElement: AnnotationElement, ...args: any[]) { @@ -731,7 +731,7 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { setIcon(iconEl, 'lucide-copy'); setTooltip(iconEl, 'Copy link'); iconEl.addEventListener('click', async () => { - const palette = lib.getColorPaletteAssociatedWithNode(popupMetaEl) + const palette = lib.getColorPaletteAssociatedWithNode(popupMetaEl); if (!palette) return; const template = plugin.settings.copyCommands[palette.actionIndex].template; @@ -821,13 +821,13 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { } return ret; - } + }; }, destroyAnnotationPopup(old) { return function () { plugin.lastAnnotationPopupChild = null; return old.call(this); - } + }; }, onContextMenu(old) { return async function (evt: MouseEvent): Promise { @@ -839,7 +839,7 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { } onContextMenu(plugin, this, evt); - } + }; }, onMobileCopy(old) { return function (this: PDFViewerChild, evt: ClipboardEvent, pageView: PDFPageView) { @@ -853,7 +853,7 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { case 'obsidian': return old.call(this, evt, pageView); } - } + }; }, onThumbnailContextMenu(old) { return function (this: PDFViewerChild, evt: MouseEvent) { @@ -862,7 +862,7 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { } onThumbnailContextMenu(plugin, this, evt); - } + }; }, getTextByRect(old) { return function (this: PDFViewerChild, pageView: PDFPageView, rect: Rect) { @@ -906,7 +906,7 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { } return text; - } + }; }, })); @@ -922,7 +922,7 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { dataTransfer.setData('text/plain', text); } }; -} +}; /** Monkey-patch ObsidianViewer so that it can open external PDF files. */ const patchObsidianViewer = (plugin: PDFPlus, pdfViewer: ObsidianViewer) => { @@ -942,7 +942,7 @@ const patchObsidianViewer = (plugin: PDFPlus, pdfViewer: ObsidianViewer) => { delete this.pdfPlusRedirect; return await old.call(this, args); - } + }; }, load(old) { return function (this: ObsidianViewer, doc: PDFDocumentProxy, ...args: any[]) { @@ -955,7 +955,7 @@ const patchObsidianViewer = (plugin: PDFPlus, pdfViewer: ObsidianViewer) => { delete this.pdfPlusCallbacksOnDocumentLoaded; return old.call(this, doc, ...args); - } + }; } })); }; @@ -970,7 +970,7 @@ const patchObsidianServices = (plugin: PDFPlus) => { spreadModeOnLoad: plugin.settings.spreadModeOnLoad, }); return old.call(this, ...args); - } + }; } })); }; diff --git a/src/patchers/pdf-outline-viewer.ts b/src/patchers/pdf-outline-viewer.ts index 0d700bf5..099daff4 100644 --- a/src/patchers/pdf-outline-viewer.ts +++ b/src/patchers/pdf-outline-viewer.ts @@ -18,9 +18,9 @@ export const patchPDFOutlineViewer = (plugin: PDFPlus, pdfOutlineViewer: PDFOutl } onOutlineItemContextMenu(plugin, child, file, item, evt); - } + }; } })); return true; -} +}; diff --git a/src/patchers/pdf-view.ts b/src/patchers/pdf-view.ts index 5ff2b9e4..3b999d72 100644 --- a/src/patchers/pdf-view.ts +++ b/src/patchers/pdf-view.ts @@ -33,7 +33,7 @@ export const patchPDFView = (plugin: PDFPlus): boolean => { ret.zoom = pdfViewer.currentScale; } return ret; - } + }; }, setState(old) { return function (state: any, result: ViewStateResult): Promise { @@ -50,7 +50,7 @@ export const patchPDFView = (plugin: PDFPlus): boolean => { } } }); - } + }; }, // Called inside onModify onLoadFile(old) { @@ -62,7 +62,7 @@ export const patchPDFView = (plugin: PDFPlus): boolean => { const state = self.getState(); const subpath = lib.viewStateToSubpath(state); return self.viewer.loadFile(file, subpath ?? undefined); - } + }; } })); @@ -78,4 +78,4 @@ export const patchPDFView = (plugin: PDFPlus): boolean => { // will be set to true when this function is called next time, and then this function will // return true return false; -} +}; diff --git a/src/patchers/workspace.ts b/src/patchers/workspace.ts index a8b50687..8b4317e6 100644 --- a/src/patchers/workspace.ts +++ b/src/patchers/workspace.ts @@ -41,7 +41,7 @@ export const patchWorkspace = (plugin: PDFPlus) => { if (pdfLeaf) { if (plugin.settings.openLinkNextToExistingPDFTab && pdfLeaf.parentSplit) { const newLeaf = app.workspace.createLeafInParent(pdfLeaf.parentSplit, -1); - return lib.workspace.openPDFLinkTextInLeaf(newLeaf, linktext, sourcePath, openViewState) + return lib.workspace.openPDFLinkTextInLeaf(newLeaf, linktext, sourcePath, openViewState); } } else if (plugin.settings.paneTypeForFirstPDFLeaf) { const newLeaf = lib.workspace.getLeaf(plugin.settings.paneTypeForFirstPDFLeaf); @@ -52,7 +52,7 @@ export const patchWorkspace = (plugin: PDFPlus) => { } return old.call(this, linktext, sourcePath, newLeaf, openViewState); - } + }; } })); diff --git a/src/pdf-backlink.ts b/src/pdf-backlink.ts index 0a3ea638..b190d59d 100644 --- a/src/pdf-backlink.ts +++ b/src/pdf-backlink.ts @@ -75,7 +75,7 @@ export class BacklinkPanePDFManager extends PDFPlusComponent { backlinkItemEl.removeEventListener('mouseout', listener); } - } + }; backlinkItemEl.addEventListener('mouseout', listener); }); @@ -237,7 +237,7 @@ export class BacklinkPanePDFPageTracker extends PDFPlusComponent { view.viewer.then((child) => { this.renderer.backlinkDom.filter = (file, linkCache) => { return child.pdfViewer.pdfViewer ? this.filter(child.pdfViewer.pdfViewer.currentPageNumber, linkCache) : true; - } + }; this.updateBacklinkDom(); this.lib.registerPDFEvent('pagechanging', child.pdfViewer.eventBus, this, (data) => { @@ -279,7 +279,7 @@ export class BacklinkPanePDFPageTracker extends PDFPlusComponent { if (!this.settings.showBacklinkToPage) { if (!params.has('selection') && !params.has('annotation') && !params.has('offset') && !params.has('rect')) return false; } - const page = +params.get('page')! + const page = +params.get('page')!; return page === pageNumber; } return false; diff --git a/src/post-process/pdf-link-like.ts b/src/post-process/pdf-link-like.ts index d6dcd4dd..2d18d894 100644 --- a/src/post-process/pdf-link-like.ts +++ b/src/post-process/pdf-link-like.ts @@ -117,7 +117,7 @@ abstract class PDFLinkLikePostProcessor implements HoverParent { linktext = await this.getLinkText(event); } catch (e) { if (e.name === 'UnknownErrorException') { - return console.warn(`${this.plugin.manifest.name}: The destination was not found in this document.`) + return console.warn(`${this.plugin.manifest.name}: The destination was not found in this document.`); } throw e; } diff --git a/src/settings.ts b/src/settings.ts index 7b98b297..b252a96d 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -50,6 +50,15 @@ const NEW_FILE_LOCATIONS = { } as const; type NewFileLocation = keyof typeof NEW_FILE_LOCATIONS; +const NEW_ATTACHMENT_LOCATIONS = { + 'root': 'Vault folder', + 'current': 'Same folder as current file', + 'folder': 'In the folder specified below', + 'subfolder': 'In subfolder under current folder', + 'obsidian': 'Same as Obsidian\'s attachment location', +} as const; +type NewAttachmentLocation = keyof typeof NEW_ATTACHMENT_LOCATIONS; + const IMAGE_EXTENSIONS = [ 'png', 'jpg', @@ -272,6 +281,12 @@ export interface PDFPlusSettings { enableBibInCanvas: boolean; copyAsSingleLine: boolean; removeWhitespaceBetweenCJChars: boolean; + // Follows the same format as Obsidian's "Default location for new attachments + // (`attachmentFolderPath`)" option, except for an empty string meaning + // following the Obsidian default + dummyFileFolderPath: string; + externalURIPatterns: string[]; + modifierToDropExternalPDFToCreateDummy: Modifier[]; vim: boolean; vimrcPath: string; vimVisualMotion: boolean; @@ -546,6 +561,12 @@ export const DEFAULT_SETTINGS: PDFPlusSettings = { enableBibInCanvas: true, copyAsSingleLine: true, removeWhitespaceBetweenCJChars: true, + dummyFileFolderPath: '', + externalURIPatterns: [ + '.*\\.pdf$', + 'https://arxiv.org/pdf/.*' + ], + modifierToDropExternalPDFToCreateDummy: ['Shift'], vim: false, vimrcPath: '', vimVisualMotion: true, @@ -606,6 +627,19 @@ export class PDFPlusSettingTab extends PluginSettingTab { this.component.registerDomEvent(item.settingEl, 'contextmenu', (evt) => { evt.preventDefault(); new Menu() + .addItem((item) => { + item.setTitle('Restore default value of this setting') + .setIcon('lucide-undo-2') + .onClick(async () => { + // @ts-ignore + this.plugin.settings[settingName] = this.plugin.getDefaultSettings()[settingName]; + await this.plugin.saveSettings(); + + this.redisplay(); + + new Notice(`${this.plugin.manifest.name}: Default setting restored. Note that some options require a restart to take effect.`, 6000); + }); + }) .addItem((item) => { item.setTitle('Copy link to this setting') .setIcon('lucide-link') @@ -631,7 +665,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { } const iconEl = createDiv(); - setting.settingEl.prepend(iconEl) + setting.settingEl.prepend(iconEl); setIcon(iconEl, icon); setting.settingEl.addClass('pdf-plus-setting-heading'); @@ -727,14 +761,14 @@ export class PDFPlusSettingTab extends PluginSettingTab { getVisibilityToggler(setting: Setting, condition: () => boolean) { const toggleVisibility = () => { condition() ? setting.settingEl.show() : setting.settingEl.hide(); - } + }; toggleVisibility(); return toggleVisibility; } showConditionally(setting: Setting | Setting[], condition: () => boolean) { const settings = Array.isArray(setting) ? setting : [setting]; - const togglers = settings.map((setting) => this.getVisibilityToggler(setting, condition)) + const togglers = settings.map((setting) => this.getVisibilityToggler(setting, condition)); this.events.on('update', () => togglers.forEach((toggler) => toggler())); return settings; } @@ -756,7 +790,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { }); if (onBlurOrEnter) { this.component.registerDomEvent(text.inputEl, 'blur', () => { - onBlurOrEnter(setting) + onBlurOrEnter(setting); }); this.component.registerDomEvent(text.inputEl, 'keypress', (evt) => { if (evt.key === 'Enter') onBlurOrEnter(setting); @@ -933,12 +967,101 @@ export class PDFPlusSettingTab extends PluginSettingTab { .then((setting) => { postProcessFolderPathSetting(setting); if (this.plugin.settings[settingName] !== 'folder') { - setting.settingEl.hide() + setting.settingEl.hide(); } }) ]; } + addAttachmentLocationSetting(settingName: KeysOfType, defaultSubfolder: string, postProcessSettings: (locationSetting: Setting, folderPathSetting: Setting, subfolderPathSetting: Setting) => any) { + let locationDropdown: DropdownComponent; + let folderPathText: TextComponent; + let subfolderPathText: TextComponent; + + const toggleVisibility = () => { + const value = locationDropdown.getValue(); + folderPathSetting.settingEl.toggle(value === 'folder'); + subfolderPathSetting.settingEl.toggle(value === 'subfolder'); + }; + const getNewAttachmentFolderPath = () => { + const value = locationDropdown.getValue() as NewAttachmentLocation; + if (value === 'root') { + return '/'; + } + if (value === 'folder') { + return folderPathText.getValue() || defaultSubfolder; + } + if (value === 'current') { + return './'; + } + if (value === 'subfolder') { + return './' + (subfolderPathText.getValue() || defaultSubfolder); + } + return ''; // An empty string means matching the Obsidian default + }; + const setValues = (value: string) => { + if (value === '') { + locationDropdown.setValue('obsidian'); + return; + } + if (value === '/') { + locationDropdown.setValue('root'); + return; + } + if (value !== '.' && value !== './') { + if (value.startsWith('./')) { + const subfolderName = value.slice(2); + locationDropdown.setValue('subfolder'); + subfolderPathText.setValue(subfolderName !== defaultSubfolder ? subfolderName : ''); + return; + } + locationDropdown.setValue('folder'); + folderPathText.setValue(value !== defaultSubfolder ? defaultSubfolder : ''); + return; + } + locationDropdown.setValue('current'); + return; + }; + + const locationSetting = this.addSetting(settingName) + .addDropdown((dropdown) => { + dropdown.onChange(async () => { + toggleVisibility(); + // @ts-ignore + this.plugin.settings[settingName] = getNewAttachmentFolderPath(); + await this.plugin.saveSettings(); + }); + dropdown.addOptions(NEW_ATTACHMENT_LOCATIONS); + locationDropdown = dropdown; + }); + const folderPathSetting = this.addSetting() + .addText((text) => { + text.setPlaceholder(defaultSubfolder) + .onChange(async () => { + // @ts-ignore + this.plugin.settings[settingName] = getNewAttachmentFolderPath(); + await this.plugin.saveSettings(); + }); + new FuzzyFolderSuggest(this.app, text.inputEl); + folderPathText = text; + }); + const subfolderPathSetting = this.addSetting() + .addText((text) => { + text.setPlaceholder(defaultSubfolder) + .onChange(async () => { + // @ts-ignore + this.plugin.settings[settingName] = getNewAttachmentFolderPath(); + await this.plugin.saveSettings(); + }); + subfolderPathText = text; + }); + + postProcessSettings(locationSetting, folderPathSetting, subfolderPathSetting); + + setValues(this.plugin.settings[settingName]); + toggleVisibility(); + } + addFundingButton() { const postProcessIcon = (iconEl: Element) => { const svg = iconEl.firstElementChild; @@ -1163,7 +1286,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { setValue(item, newValue); await this.plugin.saveSettings(); }); - }) + }); } }) .addExtraButton((button) => { @@ -1194,9 +1317,9 @@ export class PDFPlusSettingTab extends PluginSettingTab { index, defaultIndexKey, { getName: (item) => item.name, - setName: (item, value) => { item.name = value }, + setName: (item, value) => { item.name = value; }, getValue: (item) => item.template, - setValue: (item, value) => { item.template = value }, + setValue: (item, value) => { item.template = value; }, }, configs); } @@ -1255,7 +1378,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { .addButton((button) => { button.setButtonText('Open page preview settings') .onClick(() => { - this.app.setting.openTabById('page-preview') + this.app.setting.openTabById('page-preview'); }); }); } @@ -1400,11 +1523,11 @@ export class PDFPlusSettingTab extends PluginSettingTab { el.onclick = (evt) => { this.scrollTo(id, { behavior: 'smooth' }); this.updateHeaderElClassOnScroll(evt); - } + }; activeWindow.setTimeout(() => { const setting = this.items[id]; if (!name && setting) { - name = '"' + setting.nameEl.textContent + '"' + name = '"' + setting.nameEl.textContent + '"'; } el.setText(name ?? ''); }); @@ -1416,11 +1539,11 @@ export class PDFPlusSettingTab extends PluginSettingTab { el.onclick = (evt) => { this.scrollToHeading(id, { behavior: 'smooth' }); this.updateHeaderElClassOnScroll(evt); - } + }; activeWindow.setTimeout(() => { const setting = this.headings.get(id); if (!name && setting) { - name = '"' + setting.nameEl.textContent + '"' + name = '"' + setting.nameEl.textContent + '"'; } el.setText(name ?? ''); }); @@ -1473,7 +1596,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { if (linkEl) { linkEl.textContent = 'Help me keep PDF++ alive!'; linkEl.onclick = (evt) => { - this.scrollToHeading('funding', { behavior: 'smooth' }) + this.scrollToHeading('funding', { behavior: 'smooth' }); this.updateHeaderElClassOnScroll(evt); }; } @@ -1523,17 +1646,17 @@ export class PDFPlusSettingTab extends PluginSettingTab { .setDesc('In the PDF viewer, any referenced text will be highlighted for easy identification.'); this.addDesc('Try turning off the following options if you experience performance issues.'); this.addToggleSetting('highlightBacklinksInEmbed') - .setName('Highlight backlinks in PDF embeds') + .setName('Highlight backlinks in PDF embeds'); this.addToggleSetting('highlightBacklinksInCanvas') - .setName('Highlight backlinks in Canvas') + .setName('Highlight backlinks in Canvas'); this.addToggleSetting('highlightBacklinksInHoverPopover') - .setName('Highlight backlinks in hover popover previews') + .setName('Highlight backlinks in hover popover previews'); this.addDropdownSetting('selectionBacklinkVisualizeStyle', SELECTION_BACKLINK_VISUALIZE_STYLE) .setName('Highlight style') - .setDesc('How backlinks to a text selection should be visualized.') + .setDesc('How backlinks to a text selection should be visualized.'); this.addDropdownSetting('hoverHighlightAction', HOVER_HIGHLIGHT_ACTIONS, () => this.redisplay()) .setName('Action when hovering over highlighted text') - .setDesc(`Easily open backlinks or display a popover preview of it by pressing ${getModifierNameInPlatform('Mod').toLowerCase()} (by default) while hovering over a highlighted text in PDF viewer.`) + .setDesc(`Easily open backlinks or display a popover preview of it by pressing ${getModifierNameInPlatform('Mod').toLowerCase()} (by default) while hovering over a highlighted text in PDF viewer.`); this.addRequireModKeyOnHoverSetting('pdf-plus'); this.addToggleSetting('doubleClickHighlightToOpenBacklink') .setName('Double click highlighted text to open the corresponding backlink'); @@ -1549,14 +1672,14 @@ export class PDFPlusSettingTab extends PluginSettingTab { if (this.plugin.settings.paneTypeForFirstMDLeaf === 'left-sidebar' || this.plugin.settings.paneTypeForFirstMDLeaf === 'right-sidebar') { this.addToggleSetting('alwaysUseSidebar') .setName('Always use sidebar to open markdown files from highlighted text') - .setDesc(`If turned on, the ${this.plugin.settings.paneTypeForFirstMDLeaf === 'left-sidebar' ? 'left' : 'right'} sidebar will be used whether there is existing markdown tabs or not.`) + .setDesc(`If turned on, the ${this.plugin.settings.paneTypeForFirstMDLeaf === 'left-sidebar' ? 'left' : 'right'} sidebar will be used whether there is existing markdown tabs or not.`); this.addToggleSetting('singleMDLeafInSidebar') .setName('Don\'t open multiple panes in sidebar') .setDesc('Turn this on if you want to open markdown files in a single pane in the sidebar.'); } this.addSetting('ignoreExistingMarkdownTabIn') .setName('Ignore existing markdown tabs in...') - .setDesc('If some notes are opened in the ignored splits, PDF++ will still open the backlink in the way specified in the previous setting. For example, you might want to ignore the left sidebar if you are pinning a certain note (e.g. daily note) in it.') + .setDesc('If some notes are opened in the ignored splits, PDF++ will still open the backlink in the way specified in the previous setting. For example, you might want to ignore the left sidebar if you are pinning a certain note (e.g. daily note) in it.'); const splits = { 'leftSplit': 'Left sidebar', 'rightSplit': 'Right sidebar', @@ -1576,13 +1699,13 @@ export class PDFPlusSettingTab extends PluginSettingTab { }) .then((setting) => { setting.controlEl.prepend(createEl('span', { text: displayName })); - setting.settingEl.addClasses(['no-border', 'ignore-split-setting']) + setting.settingEl.addClasses(['no-border', 'ignore-split-setting']); }); } this.addToggleSetting('dontActivateAfterOpenMD') .setName('Don\'t move focus to markdown view after opening a backlink') - .setDesc('This option will be ignored when you open a link in a tab in the same split as the current tab.') + .setDesc('This option will be ignored when you open a link in a tab in the same split as the current tab.'); this.addHeading('Colors', 'color'); this.addSetting('colors') @@ -1605,7 +1728,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { this.plugin.settings.colors[''] = '#'; this.redisplay(); }); - }) + }); for (let i = 0; i < Object.keys(this.plugin.settings.colors).length; i++) { this.addColorSetting(i) .setClass('no-border'); @@ -1727,7 +1850,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { .setName('Callout icon'); - this.addHeading('PDF toolbar', 'toolbar', 'lucide-palette') + this.addHeading('PDF toolbar', 'toolbar', 'lucide-palette'); this.addToggleSetting('hoverableDropdownMenuInToolbar') .setName('Hoverable dropdown menus') .setDesc('(Not supported on smartphones) When enabled, the dropdown menus (⌄) in the PDF toolbar will be opened by hovering over the icon, and you don\'t need to click it.'); @@ -1815,7 +1938,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { this.addHeading('Context menu in PDF viewer', 'context-menu', 'lucide-mouse-pointer-click') - .setDesc('(Desktop & tablet only) Customize the behavior of the context menu that pops up when you right-click in the PDF viewer. For mobile users, see also the next section.') + .setDesc('(Desktop & tablet only) Customize the behavior of the context menu that pops up when you right-click in the PDF viewer. For mobile users, see also the next section.'); this.addToggleSetting('replaceContextMenu', () => this.redisplay()) .setName('Replace the built-in context menu with PDF++\'s custom menu'); if (!this.plugin.settings.replaceContextMenu) { @@ -1838,8 +1961,8 @@ export class PDFPlusSettingTab extends PluginSettingTab { .setName('Show the context menu right after selecting text when...') .setDesc(createFragment((el) => { el.appendText('If '); - el.appendChild(this.createLinkToHeading('auto-copy', 'auto-copy')) - el.appendText(' is enabled, it will be prioritized and the context menu will not be shown.') + el.appendChild(this.createLinkToHeading('auto-copy', 'auto-copy')); + el.appendText(' is enabled, it will be prioritized and the context menu will not be shown.'); })); { @@ -1891,14 +2014,14 @@ export class PDFPlusSettingTab extends PluginSettingTab { })); } else if (section.id === 'link') { - setting.setDesc('"Search on Google Scholar": Available when right-clicking citation links in PDFs.') + setting.setDesc('"Search on Google Scholar": Available when right-clicking citation links in PDFs.'); } else if (section.id === 'speech') { setting.setDesc(createFragment((el) => { el.appendText('Requires the '); el.createEl('a', { text: 'Text to Speech', href: 'obsidian://show-plugin?id=obsidian-tts' }); el.appendText(' plugin to be enabled.'); - })) + })); } else if (section.id === 'page') { setting.setDesc('Available when right-clicking with no text selected.'); @@ -1941,17 +2064,17 @@ export class PDFPlusSettingTab extends PluginSettingTab { } } - this.addDesc('Customize nested menus.') - this.addProductMenuSetting('selectionProductMenuConfig', 'Copy link to selection') - this.addProductMenuSetting('writeFileProductMenuConfig', `Add ${this.plugin.settings.selectionBacklinkVisualizeStyle} to file`) - this.addProductMenuSetting('annotationProductMenuConfig', 'Copy link to annotation') + this.addDesc('Customize nested menus.'); + this.addProductMenuSetting('selectionProductMenuConfig', 'Copy link to selection'); + this.addProductMenuSetting('writeFileProductMenuConfig', `Add ${this.plugin.settings.selectionBacklinkVisualizeStyle} to file`); + this.addProductMenuSetting('annotationProductMenuConfig', 'Copy link to annotation'); this.addToggleSetting('updateColorPaletteStateFromContextMenu') .setName('Update color palette from context menu') - .setDesc('In the context menu, the items (color, copy format and display text format) set in the color palette are selected by default. If this option is enabled, clicking a menu item will also update the color palette state and hence the default-selected items in the context menu as well.') + .setDesc('In the context menu, the items (color, copy format and display text format) set in the color palette are selected by default. If this option is enabled, clicking a menu item will also update the color palette state and hence the default-selected items in the context menu as well.'); } - this.addHeading('Copying on mobile', 'mobile-copy', 'lucide-smartphone') + this.addHeading('Copying on mobile', 'mobile-copy', 'lucide-smartphone'); this.addDropdownSetting('mobileCopyAction', MOBILE_COPY_ACTIONS) .setName(`Action triggered by selecting "Copy" option on mobile devices`); @@ -1975,13 +2098,13 @@ export class PDFPlusSettingTab extends PluginSettingTab { this.addSetting() .setName('Further workflow enhancements') .setDesc(createFragment((el) => { - el.appendText('See the ') - el.appendChild(this.createLinkToHeading('auto', '"Auto-copy / auto-focus / auto-paste"')) + el.appendText('See the '); + el.appendChild(this.createLinkToHeading('auto', '"Auto-copy / auto-focus / auto-paste"')); el.appendText(' section below.'); })); - this.addHeading('Other shortcut commands', 'other-hotkeys', 'lucide-layers-2') + this.addHeading('Other shortcut commands', 'other-hotkeys', 'lucide-layers-2'); this.addSetting() .then((setting) => { this.renderMarkdown([ @@ -2018,7 +2141,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { this.addHeading('Copy templates', 'template', 'lucide-copy') - .setDesc('The template format that will be used when copying a link to a selection or an annotation in PDF viewer. ') + .setDesc('The template format that will be used when copying a link to a selection or an annotation in PDF viewer. '); this.addSetting() .then((setting) => this.renderMarkdown([ // 'The template format that will be used when copying a link to a selection or an annotation in PDF viewer. ', @@ -2090,7 +2213,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { this.addIndexDropdownSetting('defaultDisplayTextFormatIndex', this.plugin.settings.displayTextFormats.map((format) => format.name), undefined, () => { this.plugin.loadStyle(); }) - .setName('Default display text format') + .setName('Default display text format'); this.addToggleSetting('syncDisplayTextFormat') .setName('Share a single display text format among all PDF viewers') .setDesc('If disabled, you can specify a different display text format for each PDF viewer from the dropdown menu in the PDF toolbar.'); @@ -2133,7 +2256,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { this.addIndexDropdownSetting('defaultColorPaletteActionIndex', this.plugin.settings.copyCommands.map((command) => command.name), undefined, () => { this.plugin.loadStyle(); }) - .setName('Default action when clicking on color palette') + .setName('Default action when clicking on color palette'); this.addToggleSetting('syncColorPaletteAction') .setName('Share a single action among all PDF viewers') .setDesc('If disabled, you can specify a different action for each PDF viewer from the dropdown menu in the PDF toolbar.'); @@ -2160,7 +2283,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { .setDesc('You can also toggle auto-focus via an icon in the left ribbon menu if the next setting is enabled.'); this.addToggleSetting('autoCopyToggleRibbonIcon', () => this.redisplay()) .setName('Show an icon to toggle auto-copy in the left ribbon menu') - .setDesc('You can also toggle this mode via a command. Reload the plugin after changing this setting to take effect.') + .setDesc('You can also toggle this mode via a command. Reload the plugin after changing this setting to take effect.'); if (this.plugin.settings.autoCopyToggleRibbonIcon) { this.addIconSetting('autoCopyIconName', false) .setName('Icon name') @@ -2263,7 +2386,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { ); this.addToggleSetting('closeSidebarWhenLostFocus') .setName('Auto-hide sidebar when it loses focus after auto-pasting') - .setDesc('After auto-pasting into a markdown file opened in the left or right sidebar, the sidebar will be automatically collapsed once it loses focus.') + .setDesc('After auto-pasting into a markdown file opened in the left or right sidebar, the sidebar will be automatically collapsed once it loses focus.'); this.addToggleSetting('openAutoFocusTargetInEditingView') .setName('Always open in editing view') @@ -2370,7 +2493,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { .setDesc('If disabled, you can specify whether to write highlights to files for each PDF viewer.'); if (this.plugin.settings.syncWriteFileToggle) { this.addToggleSetting('syncDefaultWriteFileToggle') - .setName('Share the state with newly opened PDF viewers as well') + .setName('Share the state with newly opened PDF viewers as well'); } this.addToggleSetting('enableAnnotationContentEdit', () => this.redisplay()) .setName('Enable editing annotation contents') @@ -2396,7 +2519,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { this.renderMarkdown( 'Use [modifier keys](https://help.obsidian.md/User+interface/Tabs#Open+a+link) to open PDF internal links in various ways', setting.nameEl - ) + ); }) .then((setting) => { if (this.plugin.requireModKeyForLinkHover(PDFInternalLinkPostProcessor.HOVER_LINK_SOURCE_ID)) setting.setDesc(`You may want to turn this off to avoid conflicts with hover+${modKey}.`); @@ -2526,7 +2649,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { this.renderMarkdown( 'Click PDF outline with [modifier keys](https://help.obsidian.md/User+interface/Tabs#Open+a+link) to open target section in various ways', setting.nameEl - ) + ); }) .then((setting) => { if (this.plugin.requireModKeyForLinkHover(PDFOutlineItemPostProcessor.HOVER_LINK_SOURCE_ID)) setting.setDesc(`You may want to turn this off to avoid conflicts with hover+${modKey}.`); @@ -2544,7 +2667,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { .setDesc('Reopen tabs or reload the app after changing this option.'); this.addToggleSetting('outlineContextMenu') .setName('Replace the built-in context menu in the outline with a custom one') - .setDesc('This enables you to insert a section link with a custom format by right-clicking an item in the outline. Moreover, you will be able to add, rename, or delete outline items if PDF modification is enabled.') + .setDesc('This enables you to insert a section link with a custom format by right-clicking an item in the outline. Moreover, you will be able to add, rename, or delete outline items if PDF modification is enabled.'); this.addToggleSetting('outlineDrag') .setName('Drag & drop outline item to insert link to section') .setDesc('Grab an item in the outline and drop it to a markdown file to insert a section link. Changing this option requires reopening the tabs or reloading the app.'); @@ -2598,13 +2721,13 @@ export class PDFPlusSettingTab extends PluginSettingTab { .setDesc('The copied headings will start at this level.'); - this.addHeading('PDF thumbnails', 'thumbnail', 'lucide-gallery-thumbnails') + this.addHeading('PDF thumbnails', 'thumbnail', 'lucide-gallery-thumbnails'); this.addToggleSetting('clickThumbnailWithModifierKey') .then((setting) => { this.renderMarkdown( 'Click PDF thumbnails with [modifier keys](https://help.obsidian.md/User+interface/Tabs#Open+a+link) to open target page in various ways', setting.nameEl - ) + ); }) .then((setting) => { if (this.plugin.requireModKeyForLinkHover(PDFThumbnailItemPostProcessor.HOVER_LINK_SOURCE_ID)) setting.setDesc(`You may want to turn this off to avoid conflicts with hover+${modKey}`); @@ -2616,7 +2739,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { this.showConditionally( this.addRequireModKeyOnHoverSetting(PDFThumbnailItemPostProcessor.HOVER_LINK_SOURCE_ID), () => this.plugin.settings.popoverPreviewOnThumbnailHover - ) + ); this.addToggleSetting('recordHistoryOnThumbnailClick') .setName('Record to history when clicking a thumbnail') .setDesc('Reopen tabs or reload the app after changing this option.'); @@ -2662,9 +2785,9 @@ export class PDFPlusSettingTab extends PluginSettingTab { .setName('Warn when deleting a page with backlinks'); } this.addToggleSetting('extractPageInPlace') - .setName('Remove the extracted pages from the original PDF by default') + .setName('Remove the extracted pages from the original PDF by default'); this.addToggleSetting('askExtractPageInPlace') - .setName('Ask whether to remove the extracted pages from the original PDF before extracting') + .setName('Ask whether to remove the extracted pages from the original PDF before extracting'); this.addToggleSetting('openAfterExtractPages', () => this.redisplay()) .setName('Open extracted PDF file') .setDesc('If enabled, the newly created PDF file will be opened after running the commands "Extract this page to a new file" or "Divide this PDF into two files at this page".'); @@ -2721,14 +2844,14 @@ export class PDFPlusSettingTab extends PluginSettingTab { if (this.plugin.settings.singleTabForSinglePDF) { this.addToggleSetting('dontActivateAfterOpenPDF') .setName('Don\'t move focus to PDF viewer after opening a PDF link') - .setDesc('This option will be ignored when you open a PDF link in a tab in the same split as the PDF viewer.') + .setDesc('This option will be ignored when you open a PDF link in a tab in the same split as the PDF viewer.'); this.addToggleSetting('highlightExistingTab', () => this.redisplay()) .setName('When opening a link to an already opened PDF file, highlight the tab'); if (this.plugin.settings.highlightExistingTab) { this.addSliderSetting('existingTabHighlightOpacity', 0, 1, 0.01) - .setName('Highlight opacity of an existing tab') + .setName('Highlight opacity of an existing tab'); this.addSliderSetting('existingTabHighlightDuration', 0.1, 10, 0.05) - .setName('Highlight duration of an existing tab (sec)') + .setName('Highlight duration of an existing tab (sec)'); } this.addToggleSetting('dontFitWidthWhenOpenPDFLink', () => this.events.trigger('update')) .setName('Preserve the current zoom level when opening a link to an already opened PDF file') @@ -2756,7 +2879,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { )); this.addToggleSetting('hoverPDFLinkToOpen') .setName('Open PDF link instead of showing popover preview when target PDF is already opened') - .setDesc(`Press ${getModifierNameInPlatform('Mod').toLowerCase()} while hovering a PDF link to actually open it if the target PDF is already opened in another tab.`) + .setDesc(`Press ${getModifierNameInPlatform('Mod').toLowerCase()} while hovering a PDF link to actually open it if the target PDF is already opened in another tab.`); this.addSetting() .setName('Open PDF links with an external app') .setDesc(createFragment((el) => { @@ -2786,7 +2909,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { } this.addToggleSetting('ignoreHeightParamInPopoverPreview') .setName('Ignore "height" parameter in popover preview') - .setDesc('Obsidian lets you specify the height of a PDF embed by appending "&height=..." to a link, and this also applies to popover previews. Enable this option if you want to ignore the height parameter in popover previews.') + .setDesc('Obsidian lets you specify the height of a PDF embed by appending "&height=..." to a link, and this also applies to popover previews. Enable this option if you want to ignore the height parameter in popover previews.'); this.addHeading('Embedding PDF files', 'embed', 'picture-in-picture-2'); @@ -2809,7 +2932,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { .setName('Hide sidebar in PDF embeds embeds or PDF popover previews by default'); this.addToggleSetting('noSpreadModeInEmbed') .setName('Don\'t display PDF embeds or PDF popover previews in "two page" layout') - .setDesc('Regardless of the "two page" layout setting in existing PDF viewer, PDF embeds and PDF popover previews will be always displayed in "single page" layout. You can still turn it on for each embed by clicking the "two page" button in the toolbar, if shown.') + .setDesc('Regardless of the "two page" layout setting in existing PDF viewer, PDF embeds and PDF popover previews will be always displayed in "single page" layout. You can still turn it on for each embed by clicking the "two page" button in the toolbar, if shown.'); this.addToggleSetting('noTextHighlightsInEmbed') .setName('Don\'t highlight text in a text selection embeds'); this.addToggleSetting('noAnnotationHighlightsInEmbed') @@ -2830,16 +2953,16 @@ export class PDFPlusSettingTab extends PluginSettingTab { )); this.addToggleSetting('filterBacklinksByPageDefault') .setName('Filter backlinks by page by default') - .setDesc('You can toggle this on and off with the "Show only backlinks in the current page" button at the top right of the backlinks pane.') + .setDesc('You can toggle this on and off with the "Show only backlinks in the current page" button at the top right of the backlinks pane.'); this.addToggleSetting('showBacklinkToPage') .setName('Show backlinks to the entire page') - .setDesc('If turned off, only backlinks to specific text selections, annotations or locations will be shown when filtering the backlinks page by page.') + .setDesc('If turned off, only backlinks to specific text selections, annotations or locations will be shown when filtering the backlinks page by page.'); this.addToggleSetting('highlightBacklinksPane') .setName('Hover sync (PDF viewer → Backlinks pane)') .setDesc('Hovering your mouse over highlighted text or annotation will also highlight the corresponding item in the backlink pane.'); this.addToggleSetting('highlightOnHoverBacklinkPane') .setName('Hover sync (Backlinks pane → PDF viewer)') - .setDesc('In the backlinks pane, hover your mouse over an backlink item to highlight the corresponding text or annotation in the PDF viewer. This option requires reopening or switching tabs to take effect.') + .setDesc('In the backlinks pane, hover your mouse over an backlink item to highlight the corresponding text or annotation in the PDF viewer. This option requires reopening or switching tabs to take effect.'); if (this.plugin.settings.highlightOnHoverBacklinkPane) { this.addDropdownSetting( 'backlinkHoverColor', @@ -2888,15 +3011,15 @@ export class PDFPlusSettingTab extends PluginSettingTab { this.addHeading('Integration with external apps (desktop-only)', 'external-app', 'lucide-share'); this.addToggleSetting('openPDFWithDefaultApp', () => this.redisplay()) .setName('Open PDF links with an external app') - .setDesc('Open PDF links with the OS-defined default application for PDF files.') + .setDesc('Open PDF links with the OS-defined default application for PDF files.'); if (this.plugin.settings.openPDFWithDefaultApp) { this.addToggleSetting('openPDFWithDefaultAppAndObsidian') .setName('Open PDF links in Obsidian as well') - .setDesc('Open the same PDF file both in the default app and Obsidian at the same time.') + .setDesc('Open the same PDF file both in the default app and Obsidian at the same time.'); } this.addToggleSetting('syncWithDefaultApp') .setName('Sync the external app with Obsidian') - .setDesc('When you focus on a PDF file in Obsidian, the external app will also focus on the same file.') + .setDesc('When you focus on a PDF file in Obsidian, the external app will also focus on the same file.'); this.addToggleSetting('focusObsidianAfterOpenPDFWithDefaultApp') .setName('Focus Obsidian after opening a PDF file with an external app') .setDesc('Otherwise, the focus will be moved to the external app.'); @@ -2916,6 +3039,62 @@ export class PDFPlusSettingTab extends PluginSettingTab { } + this.addHeading('Dummy PDFs for external files', 'dummy', 'lucide-file-symlink') + .then((setting) => { + this.renderMarkdown([ + 'Using dummy PDF files, you can seamlessly integrate PDF files located outside your vault as if they were inside. Note that this is an experimental feature.', + '[Learn more](https://ryotaushio.github.io/obsidian-pdf-plus/external-pdf-files.html)' + ], setting.descEl); + }); + this.addAttachmentLocationSetting('dummyFileFolderPath', 'Dummy PDFs', (locationSetting, folderPathSetting, subfolderSetting) => { + locationSetting + .setName('Default location for new dummy PDF files') + .setDesc(`Where newly created dummy PDF files are placed. If set to "${NEW_ATTACHMENT_LOCATIONS.obsidian}", dummy files will be saved in the folder specified in Obsidian settings > Files and links > Default location for new attachments.`); + folderPathSetting + .setName('Dummy file folder path') + .setDesc('Place newly created dummy PDF files in this folder.'); + subfolderSetting + .setName('Subfolder name') + .setDesc('If your file is under "vault/folder", and you set subfolder name to "attachments", dummy PDF files will be saved to "vault/folder/attachments".'); + }); + this.addSetting('modifierToDropExternalPDFToCreateDummy') + .setName('Modifier key to create a dummy PDF file on drag & drop') + .setDesc('After dragging an external PDF file, drop it on the editor while pressing this modifier key to create a dummy file and insert a link to it. You can drag a URL to a PDF file on the web from within your browser (link, URL bar, bookmark, etc.) or a PDF file on your desktop machine from your file manager (' + (Platform.isMacOS ? 'Finder' : 'File Explorer') + ' etc.). Note that on mobile, you might need to start pressing the modifier key before starting the drag operation.') + .addDropdown((dropdown) => { + const altOrCtrl = (Platform.isMacOS || Platform.isIosApp) ? 'Alt' : 'Ctrl'; + for (const keys of [[], ['Shift'], [altOrCtrl], [altOrCtrl, 'Shift']]) { + dropdown.addOption( + keys.join('+'), + keys.length + ? (keys as Modifier[]).map(getModifierNameInPlatform).join('+') + : 'None' + ); + } + dropdown + .setValue(this.plugin.settings.modifierToDropExternalPDFToCreateDummy.join('+')) + .onChange(async (value) => { + this.plugin.settings.modifierToDropExternalPDFToCreateDummy = value.split('+') as Modifier[]; + await this.plugin.saveSettings(); + }); + }); + + this.addSetting('externalURIPatterns') + .setName('URI patterns for PDF files') + .setDesc('Specify the URI pattens for PDFs in regular expressions. When dragging and dropping a URI/URL from your browser to Obsidian\'s editor, it will be used to check if the destination file is a PDF file. If you need multiple patterns, separate them with a new line.') + .addTextArea((text) => { + text.inputEl.rows = 8; + text.inputEl.cols = 30; + + text.setValue(this.plugin.settings.externalURIPatterns.join('\n')); + + this.component.registerDomEvent(text.inputEl, 'focusout', async () => { + const value = text.inputEl.value; + this.plugin.settings.externalURIPatterns = value.split('\n').map((line) => line.trim()).filter((line) => line); + await this.plugin.saveSettings(); + }); + }); + + this.addHeading('Vim keybindings', 'vim', 'vim') .then((setting) => this.renderMarkdown( @@ -3183,7 +3362,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { // avoid annotations to be not referneceable if (this.plugin.settings.enablePDFEdit && !this.plugin.settings.author) { this.plugin.settings.enablePDFEdit = false; - new Notice(`${this.plugin.manifest.name}: Cannot enable writing highlights into PDF files because the "Annotation author" option is empty.`) + new Notice(`${this.plugin.manifest.name}: Cannot enable writing highlights into PDF files because the "Annotation author" option is empty.`); } this.plugin.validateAutoFocusAndAutoPasteSettings(); diff --git a/src/toolbar.ts b/src/toolbar.ts index f90645e2..07c6860b 100644 --- a/src/toolbar.ts +++ b/src/toolbar.ts @@ -109,7 +109,7 @@ export class PDFPlusToolbar extends PDFPlusComponent { source: toolbar, value: 'page-width' }); - }) + }); }) .addItem((item) => { item.setSection('zoom') diff --git a/src/typings.d.ts b/src/typings.d.ts index 08f6bb1b..193f4beb 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -754,7 +754,8 @@ interface AppSetting extends Modal { // From https://github.com/Fevol/obsidian-typings/blob/b708f5ee3702a8622d16dab5cd0752be544c97a8/obsidian-ex.d.ts#L738 interface CustomArrayDict { - data: Record; + // From 1.7.2, this is a map instead of a record. + data: Map; add: (key: string, value: T) => void; remove: (key: string, value: T) => void; @@ -1144,6 +1145,7 @@ declare module 'obsidian' { getConfig(name: 'tabSize'): number; getConfig(name: 'alwaysUpdateLinks'): boolean; getConfig(name: 'newFileLocation'): 'root' | 'current' | 'folder'; + getConfig(name: 'attachmentFolderPath'): string; getAvailablePath(pathWithoutExtension: string, extension: string): string; } diff --git a/src/user-script/context.ts b/src/user-script/context.ts index 522068fc..c4773f51 100644 --- a/src/user-script/context.ts +++ b/src/user-script/context.ts @@ -74,12 +74,12 @@ export class UserScriptContext extends PDFPlusComponent { if (!this.file) return ''; if (typeof this.page !== 'number') return ''; - const res = this.lib.copyLink.getPageAndTextRangeFromSelection(this.doc.getSelection()) + const res = this.lib.copyLink.getPageAndTextRangeFromSelection(this.doc.getSelection()); if (!res) return ''; const { page, selection } = res; if (!selection) return ''; - const subpath = paramsToSubpath({ page, selection: `${selection.beginIndex},${selection.beginOffset},${selection.endIndex},${selection.endOffset}`, color: color ? color.toLowerCase() : undefined }) + const subpath = paramsToSubpath({ page, selection: `${selection.beginIndex},${selection.beginOffset},${selection.endIndex},${selection.endOffset}`, color: color ? color.toLowerCase() : undefined }); return this.lib.copyLink.getTextToCopy(this.child, copyFormat, displayTextFormat, this.file, this.page, subpath, this.text, color ? color.toLowerCase() : ''); } diff --git a/src/utils/color.ts b/src/utils/color.ts index f9a57883..235910e7 100644 --- a/src/utils/color.ts +++ b/src/utils/color.ts @@ -26,7 +26,7 @@ export function rgbToHex(rgb: RGB) { export function rgbStringToObject(rgbString: string): RGB { const [r, g, b] = rgbString // "R, G, B" .split(',') - .map((s) => parseInt(s.trim())) // [R, G, B]; + .map((s) => parseInt(s.trim())); // [R, G, B]; return { r, g, b }; } @@ -34,7 +34,7 @@ export function getObsidianDefaultHighlightColorRGB(): RGB { const [r, g, b] = getComputedStyle(document.body) .getPropertyValue('--text-highlight-bg-rgb') // "R, G, B" .split(',') - .map((s) => parseInt(s.trim())) // [R, G, B]; + .map((s) => parseInt(s.trim())); // [R, G, B]; return { r, g, b }; } diff --git a/src/utils/events.ts b/src/utils/events.ts index c27a003e..febbf26a 100644 --- a/src/utils/events.ts +++ b/src/utils/events.ts @@ -1,3 +1,4 @@ +import { Modifier } from 'obsidian'; import { App, Component, Keymap, Platform } from 'obsidian'; @@ -102,7 +103,7 @@ export function onModKeyPress(evt: MouseEvent | TouchEvent | KeyboardEvent, targ doc.removeEventListener('keydown', onKeyDown); doc.removeEventListener('mouseover', onMouseOver); doc.removeEventListener('mouseleave', onMouseLeave); - } + }; // Watch for the mod key press const onKeyDown = (e: KeyboardEvent) => { @@ -122,7 +123,7 @@ export function onModKeyPress(evt: MouseEvent | TouchEvent | KeyboardEvent, targ // Stop watching for the mod key press when the mouse leaves the document const onMouseLeave = (e: MouseEvent) => { if (removed) return; - if (e.target === doc) removeHandlers() + if (e.target === doc) removeHandlers(); }; doc.addEventListener('keydown', onKeyDown); @@ -208,3 +209,14 @@ export function hover(target: HTMLElement, mod?: boolean, options?: MouseEventIn ...options }); } + +const MODIFIERS: Modifier[] = ['Mod', 'Ctrl', 'Meta', 'Shift', 'Alt']; + +export function matchModifiers(evt: MouseEvent, modifiers: Modifier[]): boolean { + return MODIFIERS.every((modifier) => { + if (modifiers.includes(modifier)) { + return Keymap.isModifier(evt, modifier); + } + return !Keymap.isModifier(evt, modifier); + }); +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 1ca9cd4e..0f0a02f0 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -79,10 +79,10 @@ export function getTextLayerNode(pageEl: HTMLElement, node: Node) { while (n = n.parentNode) { if (n === pageEl) return null; if (n.instanceOf(HTMLElement) && n.hasClass('textLayerNode')) - return n + return n; } - return null + return null; } // Taken from app.js. @@ -144,7 +144,7 @@ export function* toPDFCoords(pageView: PDFPageView, screenCoords: Iterable<{ x: const pageRect = pageEl.getBoundingClientRect(); for (const { x, y } of screenCoords) { - const xRelativeToPage = x - (pageRect.left + borderLeft + paddingLeft) + const xRelativeToPage = x - (pageRect.left + borderLeft + paddingLeft); const yRelativeToPage = y - (pageRect.top + borderTop + paddingTop); yield pageView.getPagePoint(xRelativeToPage, yRelativeToPage) as [number, number]; } @@ -196,7 +196,7 @@ export function getModifierDictInPlatform(): Partial> { dict[modifier] = name; } } - return dict + return dict; } export function isModifierName(name: string): name is Modifier { @@ -254,7 +254,7 @@ export function parsePDFSubpath(subpath: string): { page: number } | { page: num const page = +params.get('page')!; if (isNaN(page)) return null; if (params.has('selection')) { - const selectionPos = params.get('selection')!.split(',').map((s) => parseInt(s.trim())) + const selectionPos = params.get('selection')!.split(',').map((s) => parseInt(s.trim())); if (selectionPos.length !== 4 || selectionPos.some((pos) => isNaN(pos))) return null; const [beginIndex, beginOffset, endIndex, endOffset] = selectionPos; return { page, beginIndex, beginOffset, endIndex, endOffset }; @@ -337,7 +337,7 @@ export function isAncestorOf) { options = { japanese: true, korean: true, ...options }; - let pattern = '' + let pattern = ''; // CJK Unified Ideographs pattern += '\\u4e00-\\u9fff'; @@ -464,7 +464,7 @@ export function binarySearchForRangeStartingWith(array: T[], prefix: string, const { index: to } = binarySearch(array, cmp, { findLast: true, ...options, ...{ from } }); return { from, to }; } - return null + return null; } export function areOverlapping(range1: { from: number, to: number }, range2: { from: number, to: number }) { diff --git a/src/utils/maps.ts b/src/utils/maps.ts index 34161bd1..89b49880 100644 --- a/src/utils/maps.ts +++ b/src/utils/maps.ts @@ -60,7 +60,7 @@ export class BidirectionalMultiValuedMap { const values = this.keyToValues.get(key); if (values) { for (const value of values) { - const keys = this.valueToKeys.get(value) + const keys = this.valueToKeys.get(value); if (!keys) { throw new Error('Value has no keys'); } diff --git a/src/utils/menu.ts b/src/utils/menu.ts index 5b74e119..c5b32e45 100644 --- a/src/utils/menu.ts +++ b/src/utils/menu.ts @@ -45,7 +45,7 @@ export function addProductMenuItems(rootMenu: Menu, itemAdders: ((menu: Menu) => } } } - } + }; addItemsToMenu(rootMenu, 0); } @@ -80,7 +80,7 @@ export function fixOpenSubmenu(menu: Menu, timeout?: number) { this.closeSubmenu(); } return Menu.prototype.openSubmenu.call(this, item); - } + }; menu.openSubmenuSoon = debounce(menu.openSubmenu.bind(menu), timeout ?? 250, true); } diff --git a/src/utils/suggest.ts b/src/utils/suggest.ts index e6896622..6df0d78d 100644 --- a/src/utils/suggest.ts +++ b/src/utils/suggest.ts @@ -13,7 +13,7 @@ type FuzzyInputSuggestOptions = { const DEFAULT_FUZZY_INPUT_SUGGEST_OPTIONS: FuzzyInputSuggestOptions = { blurOnSelect: true, closeOnSelect: true, -} +}; export abstract class FuzzyInputSuggest extends AbstractInputSuggest> { @@ -31,7 +31,7 @@ export abstract class FuzzyInputSuggest extends AbstractInputSuggest[] = []; @@ -83,7 +83,7 @@ export class FuzzyFileSuggest extends FuzzyInputSuggest { export class FuzzyFolderSuggest extends FuzzyInputSuggest { getItems() { - return this.app.vault.getAllLoadedFiles().filter((file): file is TFolder => file instanceof TFolder) + return this.app.vault.getAllLoadedFiles().filter((file): file is TFolder => file instanceof TFolder); } getItemText(file: TFolder) { diff --git a/src/vim/ex-commands.ts b/src/vim/ex-commands.ts index 509da6a0..49e2f51c 100644 --- a/src/vim/ex-commands.ts +++ b/src/vim/ex-commands.ts @@ -123,7 +123,7 @@ export const exCommands = (vim: VimBindings): ExCommand[] => { if (targets.includes('all')) targets = ['link', 'annot', 'backlink']; vim.hintMode.setTarget(...targets.map((target) => { switch (target) { - case 'link': return VimHintTarget.Link + case 'link': return VimHintTarget.Link; case 'annot': return VimHintTarget.NonLinkAnnot; case 'backlink': return VimHintTarget.BacklinkHighlight; default: throw Error(`Unknown hint target: ${target}`); @@ -134,14 +134,14 @@ export const exCommands = (vim: VimBindings): ExCommand[] => { } } ]; -} +}; const lint = (str: string, indentSize = 12, escapeAngleBrackets = true) => { str = str .replace(new RegExp(`^ {${indentSize}}`, 'gm'), '') // remove indentation - .replace(/^\s*/, '') // remove leading whitespaces and newlines + .replace(/^\s*/, ''); // remove leading whitespaces and newlines return escapeAngleBrackets ? str.replace(/([<>])/g, '\\$1') : str; -} +}; const mapDesc = (signature: string, modes: string[], noremap = false) => `:${signature} - Map to ${noremap ? 'non-recusively ' : ''}in ${modes.length > 1 ? modes.slice(0, -1).join(', ') + ' and ' + modes.at(-1)! + ' modes' : modes[0] + ' mode'}. If is an ex-command, it must be start with ":".`; diff --git a/src/vim/hint.ts b/src/vim/hint.ts index 335567a6..e4c32eb2 100644 --- a/src/vim/hint.ts +++ b/src/vim/hint.ts @@ -186,4 +186,4 @@ const isCloseTo = (prevLinkEl: HTMLElement, thisLinkEl: HTMLElement) => { || Math.abs(thisRect.bottom - prevRect.top) < xThreshold ) ); -} +}; diff --git a/src/vim/hintnames.ts b/src/vim/hintnames.ts index f2deb75f..fdbfedaf 100644 --- a/src/vim/hintnames.ts +++ b/src/vim/hintnames.ts @@ -218,7 +218,7 @@ function* hintnames_simple( for (let taglen = 1; true; taglen++) { yield* map(permutationsWithReplacement(hintchars, taglen), e => e.join(''), - ) + ); } } @@ -298,5 +298,5 @@ function* map(arr: Iterable, func: (v: T) => any) { * Taken from https://github.com/tridactyl/tridactyl/blob/4a4c9c7306b436611088b6ff2dceff77e7ccbfd6/src/lib/number.mod.ts#L9-L12 */ function knuth_mod(dividend: number, divisor: number) { - return dividend - divisor * Math.floor(dividend / divisor) + return dividend - divisor * Math.floor(dividend / divisor); } diff --git a/src/vim/outline.ts b/src/vim/outline.ts index 89f630a0..1fae8617 100644 --- a/src/vim/outline.ts +++ b/src/vim/outline.ts @@ -106,7 +106,7 @@ export class VimOutlineMode extends VimBindingsMode { if (outline) { func(outline, n); } - } + }; } changeActiveItemTo(newActiveItem: PDFOutlineTreeNode) { diff --git a/src/vim/scope.ts b/src/vim/scope.ts index 8fbcd5c1..73547bd3 100644 --- a/src/vim/scope.ts +++ b/src/vim/scope.ts @@ -5,7 +5,7 @@ import { binarySearch, binarySearchForRangeStartingWith, isTargetHTMLElement, is const isTargetTypable = (evt: KeyboardEvent) => { return isTargetHTMLElement(evt, evt.target) && isTypable(evt.target); -} +}; export type VimCommand = (n?: number) => any; export type VimKeymap = { keys: string, func: VimCommand }; @@ -15,7 +15,7 @@ export class VimScope extends Scope { currentMode: string | null = null; currentKeys: string = ''; searchFrom = 0; - searchTo = -1 + searchTo = -1; onEscapeCallbacks: ((isRealEscape: boolean) => any)[] = []; escapeAliases: string[] = []; typableModes: string[] = []; @@ -30,7 +30,7 @@ export class VimScope extends Scope { for (const mode of modes) { if (!this.modeToKeymaps.hasOwnProperty(mode)) { this.modeToKeymaps[mode] = Object.entries(keymapDict) - .map(([keys, func]) => { return { keys, func } }) + .map(([keys, func]) => { return { keys, func }; }) .sort(cmp); continue; } @@ -61,7 +61,7 @@ export class VimScope extends Scope { .map(([from, to]) => [ from, (n?: number) => { - const { found, index } = binarySearch(this.modeToKeymaps[mode], (map) => stringCompare(to, map.keys)) + const { found, index } = binarySearch(this.modeToKeymaps[mode], (map) => stringCompare(to, map.keys)); if (found) { const func = this.modeToKeymaps[mode][index].func; return func(n); @@ -78,7 +78,7 @@ export class VimScope extends Scope { if (this.modeToKeymaps.hasOwnProperty(mode)) { for (const from in fromTo) { const to = fromTo[from]; - const { found, index } = binarySearch(this.modeToKeymaps[mode], (map) => stringCompare(to, map.keys)) + const { found, index } = binarySearch(this.modeToKeymaps[mode], (map) => stringCompare(to, map.keys)); if (found) { const keymap = this.modeToKeymaps[mode][index]; this.registerKeymaps([mode], { [from]: keymap.func }); @@ -92,7 +92,7 @@ export class VimScope extends Scope { for (const mode of modes) { if (this.modeToKeymaps.hasOwnProperty(mode)) { for (const key of keys) { - const { found, index } = binarySearch(this.modeToKeymaps[mode], (map) => stringCompare(key, map.keys)) + const { found, index } = binarySearch(this.modeToKeymaps[mode], (map) => stringCompare(key, map.keys)); if (found) { this.modeToKeymaps[mode].splice(index, 1); } diff --git a/src/vim/scroll.ts b/src/vim/scroll.ts index 63cf946b..a74d3bd5 100644 --- a/src/vim/scroll.ts +++ b/src/vim/scroll.ts @@ -77,14 +77,14 @@ export class ScrollController { if (!this.viewerContainerEl) return; const pageDiv = this.getPageDiv(); if (!pageDiv) return; - this.viewerContainerEl.scrollTo({ top: pageDiv.offsetTop, behavior: (this.settings.vimSmoothScroll ? 'smooth' : 'instant') as ScrollBehavior }) + this.viewerContainerEl.scrollTo({ top: pageDiv.offsetTop, behavior: (this.settings.vimSmoothScroll ? 'smooth' : 'instant') as ScrollBehavior }); } scrollToBottom() { if (!this.viewerContainerEl) return; const pageDiv = this.getPageDiv(); if (!pageDiv) return; - this.viewerContainerEl.scrollTo({ top: pageDiv.offsetTop + pageDiv.offsetHeight - this.viewerContainerEl.clientHeight, behavior: (this.settings.vimSmoothScroll ? 'smooth' : 'instant') as ScrollBehavior }) + this.viewerContainerEl.scrollTo({ top: pageDiv.offsetTop + pageDiv.offsetHeight - this.viewerContainerEl.clientHeight, behavior: (this.settings.vimSmoothScroll ? 'smooth' : 'instant') as ScrollBehavior }); } /** Here "page" does not mean the PDF page but the "visual page", i.e. the region of the screen that is currently visible. */ diff --git a/src/vim/vim.ts b/src/vim/vim.ts index 3ccca094..b4d0900e 100644 --- a/src/vim/vim.ts +++ b/src/vim/vim.ts @@ -162,13 +162,13 @@ export class VimBindings extends PDFPlusComponent { onload() { this.lib.workspace.iteratePDFViews((view) => { - view.viewer === this.viewer && (view.scope = this.vimScope) + view.viewer === this.viewer && (view.scope = this.vimScope); }); } onunload() { this.lib.workspace.iteratePDFViews((view) => { - view.viewer === this.viewer && (view.scope = this.viewer.scope) + view.viewer === this.viewer && (view.scope = this.viewer.scope); }); } diff --git a/src/vim/visual.ts b/src/vim/visual.ts index 2f0136bc..8668ec28 100644 --- a/src/vim/visual.ts +++ b/src/vim/visual.ts @@ -138,7 +138,7 @@ export class VimVisualMode extends VimBindingsMode { } getTextDivContainingNode(node: Node) { - const element = node.instanceOf(Element) ? node : node.parentElement + const element = node.instanceOf(Element) ? node : node.parentElement; if (!element) return null; const textDiv = element.closest('.textLayerNode'); @@ -188,7 +188,7 @@ export class VimVisualMode extends VimBindingsMode { const isNodeNonemptyTextDiv = (node: Node): node is HTMLElement => { return node.instanceOf(HTMLElement) && node.hasClass('textLayerNode') && !!node.textContent; - } + }; if (offset < 0) { let prevDiv = textDiv.previousSibling; diff --git a/styles.css b/styles.css index c790d48b..bca8eb14 100644 --- a/styles.css +++ b/styles.css @@ -467,6 +467,10 @@ body.pdf-plus-backlink-selection-underline { } } +.pdf-plus-restore-default-modal { + user-select: text; +} + .pdf-content-container { --sidebar-width: var(--pdf-plus-sidebar-width, 140px); }