mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
490 lines
16 KiB
TypeScript
490 lines
16 KiB
TypeScript
import { ButtonComponent, TFile, ToggleComponent } from "obsidian";
|
|
import ExcalidrawView from "../ExcalidrawView";
|
|
import ExcalidrawPlugin from "../main";
|
|
import { getPDFDoc } from "src/utils/FileUtils";
|
|
import { Modal, Setting, TextComponent } from "obsidian";
|
|
import { FileSuggestionModal } from "../Components/Suggesters/FileSuggestionModal";
|
|
import { getEA } from "src";
|
|
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
|
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
|
|
import { t } from "src/lang/helpers";
|
|
|
|
export class InsertPDFModal extends Modal {
|
|
private borderBox: boolean = true;
|
|
private frame: boolean = false;
|
|
private gapSize:number = 20;
|
|
private groupPages: boolean = false;
|
|
private direction: "down" | "right" = "right";
|
|
private numColumns: number = 1;
|
|
private numRows: number = 1;
|
|
private lockAfterImport: boolean = true;
|
|
private pagesToImport:number[] = [];
|
|
private pageDimensions: {width: number, height: number} = {width: 0, height: 0};
|
|
private importScale = 0.3;
|
|
private imageSizeMessage: HTMLElement;
|
|
private pdfDoc: any;
|
|
private pdfFile: TFile;
|
|
private dirty: boolean = false;
|
|
|
|
constructor(
|
|
private plugin: ExcalidrawPlugin,
|
|
private view: ExcalidrawView,
|
|
) {
|
|
super(plugin.app);
|
|
}
|
|
|
|
open (file?: TFile) {
|
|
if(file && file.extension.toLowerCase() === "pdf") {
|
|
this.pdfFile = file;
|
|
}
|
|
super.open();
|
|
}
|
|
|
|
onOpen(): void {
|
|
this.containerEl.classList.add("excalidraw-release");
|
|
this.titleEl.setText(`Import PDF`);
|
|
this.createForm();
|
|
}
|
|
|
|
async onClose() {
|
|
if(this.dirty) {
|
|
this.plugin.settings.pdfImportScale = this.importScale;
|
|
this.plugin.settings.pdfBorderBox = this.borderBox;
|
|
this.plugin.settings.pdfFrame = this.frame;
|
|
this.plugin.settings.pdfGapSize = this.gapSize;
|
|
this.plugin.settings.pdfGroupPages = this.groupPages;
|
|
this.plugin.settings.pdfNumColumns = this.numColumns;
|
|
this.plugin.settings.pdfNumRows = this.numRows;
|
|
this.plugin.settings.pdfDirection = this.direction;
|
|
this.plugin.settings.pdfLockAfterImport = this.lockAfterImport;
|
|
await this.plugin.saveSettings();
|
|
}
|
|
if(this.pdfDoc) {
|
|
this.pdfDoc.destroy();
|
|
this.pdfDoc = null;
|
|
}
|
|
this.plugin = null;
|
|
this.view = null;
|
|
this.app = null;
|
|
this.imageSizeMessage.remove();
|
|
this.setImageSizeMessage = null;
|
|
}
|
|
|
|
private async getPageDimensions (pdfDoc: any) {
|
|
try {
|
|
const scale = this.plugin.settings.pdfScale;
|
|
const canvas = createEl("canvas");
|
|
const page = await pdfDoc.getPage(1);
|
|
// Set scale
|
|
const viewport = page.getViewport({ scale });
|
|
this.pageDimensions.height = viewport.height;
|
|
this.pageDimensions.width = viewport.width;
|
|
|
|
//https://github.com/excalidraw/excalidraw/issues/4036
|
|
canvas.width = 0;
|
|
canvas.height = 0;
|
|
this.setImageSizeMessage();
|
|
} catch(e) {
|
|
console.log(e);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates a list of numbers from page ranges representing the pages to import.
|
|
* sets the pagesToImport property.
|
|
* @param pageRanges A string representing the pages to import. e.g.: 1,3-5,7,9-10
|
|
* @returns A list of numbers representing the pages to import.
|
|
*/
|
|
private createPageListFromString(pageRanges:string):number[] {
|
|
const cleanNonDigits = (str:string) => str.replace(/\D/g, "");
|
|
this.pagesToImport = [];
|
|
const pageRangesArray:string[] = pageRanges.split(",");
|
|
pageRangesArray.forEach((pageRange) => {
|
|
const pageRangeArray = pageRange.split("-");
|
|
if(pageRangeArray.length === 1) {
|
|
const page = parseInt(cleanNonDigits(pageRangeArray[0]));
|
|
!isNaN(page) && this.pagesToImport.push(page);
|
|
} else if(pageRangeArray.length === 2) {
|
|
|
|
const start = parseInt(cleanNonDigits(pageRangeArray[0]));
|
|
const end = parseInt(cleanNonDigits(pageRangeArray[1]));
|
|
if(isNaN(start) || isNaN(end)) return;
|
|
for(let i = start; i <= end; i++) {
|
|
this.pagesToImport.push(i);
|
|
}
|
|
}
|
|
});
|
|
return this.pagesToImport;
|
|
}
|
|
|
|
private setImageSizeMessage = () => this.imageSizeMessage.innerText = `${Math.round(this.pageDimensions.width*this.importScale)} x ${Math.round(this.pageDimensions.height*this.importScale)}`;
|
|
|
|
async createForm() {
|
|
await this.plugin.loadSettings();
|
|
this.borderBox = this.plugin.settings.pdfBorderBox;
|
|
this.frame = this.plugin.settings.pdfFrame;
|
|
this.gapSize = this.plugin.settings.pdfGapSize;
|
|
this.groupPages = this.plugin.settings.pdfGroupPages;
|
|
this.numColumns = this.plugin.settings.pdfNumColumns;
|
|
this.numRows = this.plugin.settings.pdfNumRows;
|
|
this.direction = this.plugin.settings.pdfDirection;
|
|
this.lockAfterImport = this.plugin.settings.pdfLockAfterImport;
|
|
this.importScale = this.plugin.settings.pdfImportScale;
|
|
|
|
const ce = this.contentEl;
|
|
|
|
|
|
let numPagesMessage: HTMLParagraphElement;
|
|
let numPages: number;
|
|
let importButton: ButtonComponent;
|
|
let importMessage: HTMLElement;
|
|
|
|
const importButtonMessages = () => {
|
|
if(!this.pdfDoc) {
|
|
importMessage.innerText = t("IPM_SELECT_PDF");
|
|
importButton.buttonEl.style.display="none";
|
|
return;
|
|
}
|
|
if(this.pagesToImport.length === 0) {
|
|
importButton.buttonEl.style.display="none";
|
|
importMessage.innerText = t("IPM_SELECT_PAGES_TO_IMPORT");
|
|
return
|
|
}
|
|
if(Math.max(...this.pagesToImport) <= this.pdfDoc.numPages) {
|
|
importButton.buttonEl.style.display="block";
|
|
importMessage.innerText = "";
|
|
return;
|
|
}
|
|
else {
|
|
importButton.buttonEl.style.display="none";
|
|
importMessage.innerText = `The selected document has ${this.pdfDoc.numPages} pages. Please select pages between 1 and ${this.pdfDoc.numPages}`;
|
|
return
|
|
}
|
|
}
|
|
|
|
const numPagesMessages = () => {
|
|
if(numPages === 0) {
|
|
numPagesMessage.innerText = t("IPM_SELECT_PDF");
|
|
return;
|
|
}
|
|
numPagesMessage.innerHTML = `There are <b>${numPages}</b> pages in the selected document.`;
|
|
}
|
|
|
|
let pageRangesTextComponent: TextComponent
|
|
let importPagesMessage: HTMLParagraphElement;
|
|
|
|
const rangeOnChange = (value:string) => {
|
|
const pages = this.createPageListFromString(value);
|
|
if(pages.length > 15) {
|
|
importPagesMessage.innerHTML = `You are importing <b>${pages.length}</b> pages. ⚠️ This may take a while. ⚠️`;
|
|
} else {
|
|
importPagesMessage.innerHTML = `You are importing <b>${pages.length}</b> pages.`;
|
|
}
|
|
importButtonMessages();
|
|
}
|
|
|
|
const setFile = async (file: TFile) => {
|
|
if(this.pdfDoc) await this.pdfDoc.destroy();
|
|
this.pdfDoc = null;
|
|
|
|
if(file) {
|
|
this.pdfDoc = await getPDFDoc(file);
|
|
this.pdfFile = file;
|
|
if(this.pdfDoc) {
|
|
numPages = this.pdfDoc.numPages;
|
|
pageRangesTextComponent.setValue(`1-${numPages}`);
|
|
rangeOnChange(`1-${numPages}`);
|
|
importButtonMessages();
|
|
numPagesMessages();
|
|
this.getPageDimensions(this.pdfDoc);
|
|
} else {
|
|
importButton.setDisabled(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
const search = new TextComponent(ce);
|
|
search.inputEl.style.width = "100%";
|
|
const suggester = new FileSuggestionModal(
|
|
this.app,
|
|
search,this.app.vault.getFiles().filter((f: TFile) => f.extension.toLowerCase() === "pdf"),
|
|
this.plugin
|
|
);
|
|
search.onChange(async () => {
|
|
const file = suggester.getSelectedItem();
|
|
await setFile(file);
|
|
});
|
|
|
|
numPagesMessage = ce.createEl("p", {text: ""});
|
|
numPagesMessages();
|
|
new Setting(ce)
|
|
.setName(t("IPM_PAGES_TO_IMPORT_NAME"))
|
|
.setDesc("e.g.: 1,3-5,7,9-10")
|
|
.addText(text => {
|
|
pageRangesTextComponent = text;
|
|
text
|
|
.setValue("")
|
|
.onChange((value) => rangeOnChange(value))
|
|
text.inputEl.style.width = "100%";
|
|
})
|
|
importPagesMessage = ce.createEl("p", {text: ""});
|
|
|
|
let bbToggle: ToggleComponent;
|
|
let fToggle: ToggleComponent;
|
|
let laiToggle: ToggleComponent;
|
|
|
|
this.frame = this.borderBox ? false : this.frame;
|
|
|
|
new Setting(ce)
|
|
.setName(t("IPM_ADD_BORDER_BOX_NAME"))
|
|
.addToggle(toggle => {
|
|
bbToggle = toggle;
|
|
toggle
|
|
.setValue(this.borderBox)
|
|
.onChange((value) => {
|
|
this.borderBox = value;
|
|
if(value) {
|
|
this.frame = false;
|
|
fToggle.setValue(false);
|
|
}
|
|
this.dirty = true;
|
|
})
|
|
})
|
|
|
|
new Setting(ce)
|
|
.setName(t("IPM_ADD_FRAME_NAME"))
|
|
.setDesc(t("IPM_ADD_FRAME_DESC"))
|
|
.addToggle(toggle => {
|
|
fToggle = toggle;
|
|
toggle
|
|
.setValue(this.frame)
|
|
.onChange((value) => {
|
|
this.frame = value;
|
|
if(value) {
|
|
this.borderBox = false;
|
|
bbToggle.setValue(false);
|
|
if(!this.lockAfterImport) {
|
|
this.lockAfterImport = true;
|
|
laiToggle.setValue(true);
|
|
}
|
|
}
|
|
this.dirty = true;
|
|
})
|
|
})
|
|
|
|
new Setting(ce)
|
|
.setName(t("IPM_GROUP_PAGES_NAME"))
|
|
.setDesc(t("IPM_GROUP_PAGES_DESC"))
|
|
.addToggle(toggle => toggle
|
|
.setValue(this.groupPages)
|
|
.onChange((value) => {
|
|
this.groupPages = value
|
|
this.dirty = true;
|
|
}))
|
|
|
|
|
|
new Setting(ce)
|
|
.setName("Lock pages on canvas after import")
|
|
.addToggle(toggle => {
|
|
laiToggle = toggle;
|
|
toggle
|
|
.setValue(this.lockAfterImport)
|
|
.onChange((value) => {
|
|
this.lockAfterImport = value
|
|
this.dirty = true;
|
|
})
|
|
})
|
|
|
|
let numColumnsSetting: Setting;
|
|
let numRowsSetting: Setting;
|
|
const colRowVisibility = () => {
|
|
switch(this.direction) {
|
|
case "down":
|
|
numColumnsSetting.settingEl.style.display = "none";
|
|
numRowsSetting.settingEl.style.display = "";
|
|
break;
|
|
case "right":
|
|
numColumnsSetting.settingEl.style.display = "";
|
|
numRowsSetting.settingEl.style.display = "none";
|
|
break;
|
|
}
|
|
}
|
|
|
|
new Setting(ce)
|
|
.setName("Import direction")
|
|
.addDropdown(dropdown => dropdown
|
|
.addOptions({
|
|
"down": "Top > Down",
|
|
"right": "Left > Right"
|
|
})
|
|
.setValue(this.direction)
|
|
.onChange(value => {
|
|
this.direction = value as "down" | "right";
|
|
colRowVisibility();
|
|
this.dirty = true;
|
|
}))
|
|
|
|
let columnsText: HTMLDivElement;
|
|
numColumnsSetting = new Setting(ce);
|
|
numColumnsSetting
|
|
.setName("Number of columns")
|
|
.addSlider(slider => slider
|
|
.setLimits(1, 100, 1)
|
|
.setValue(this.numColumns)
|
|
.onChange(value => {
|
|
this.numColumns = value;
|
|
columnsText.innerText = ` ${value.toString()}`;
|
|
this.dirty = true;
|
|
}))
|
|
.settingEl.createDiv("", (el) => {
|
|
columnsText = el;
|
|
el.style.minWidth = "2.3em";
|
|
el.style.textAlign = "right";
|
|
el.innerText = ` ${this.numColumns.toString()}`;
|
|
});
|
|
|
|
let rowsText: HTMLDivElement;
|
|
numRowsSetting = new Setting(ce);
|
|
numRowsSetting
|
|
.setName("Number of rows")
|
|
.addSlider(slider => slider
|
|
.setLimits(1, 100, 1)
|
|
.setValue(this.numRows)
|
|
.onChange(value => {
|
|
this.numRows = value;
|
|
rowsText.innerText = ` ${value.toString()}`;
|
|
this.dirty = true;
|
|
}))
|
|
.settingEl.createDiv("", (el) => {
|
|
rowsText = el;
|
|
el.style.minWidth = "2.3em";
|
|
el.style.textAlign = "right";
|
|
el.innerText = ` ${this.numRows.toString()}`;
|
|
});
|
|
colRowVisibility();
|
|
|
|
let gapSizeText: HTMLDivElement;
|
|
new Setting(ce)
|
|
.setName("Size of gap between pages")
|
|
.addSlider(slider => slider
|
|
.setLimits(10, 200, 10)
|
|
.setValue(this.gapSize)
|
|
.onChange(value => {
|
|
this.gapSize = value;
|
|
gapSizeText.innerText = ` ${value.toString()}`;
|
|
this.dirty = true;
|
|
}))
|
|
.settingEl.createDiv("", (el) => {
|
|
gapSizeText = el;
|
|
el.style.minWidth = "2.3em";
|
|
el.style.textAlign = "right";
|
|
el.innerText = ` ${this.gapSize.toString()}`;
|
|
});
|
|
|
|
const importSizeSetting = new Setting(ce)
|
|
.setName("Imported page size")
|
|
.setDesc(`${this.pageDimensions.width*this.importScale} x ${this.pageDimensions.height*this.importScale}`)
|
|
.addSlider(slider => slider
|
|
.setLimits(0.1, 1.5, 0.1)
|
|
.setValue(this.importScale)
|
|
.onChange(value => {
|
|
this.importScale = value;
|
|
this.dirty = true;
|
|
this.setImageSizeMessage();
|
|
}))
|
|
|
|
this.imageSizeMessage = importSizeSetting.descEl;
|
|
|
|
const actionButton = new Setting(ce)
|
|
.setDesc("Select a document first")
|
|
.addButton(button => {
|
|
button
|
|
.setButtonText("Import PDF")
|
|
.setCta()
|
|
.onClick(async () => {
|
|
const ea = getEA(this.view) as ExcalidrawAutomate;
|
|
let column = 0;
|
|
let row = 0;
|
|
const imgWidth = Math.round(this.pageDimensions.width*this.importScale);
|
|
const imgHeight = Math.round(this.pageDimensions.height*this.importScale);
|
|
for(let i = 0; i < this.pagesToImport.length; i++) {
|
|
const page = this.pagesToImport[i];
|
|
importMessage.innerText = `Importing page ${page} (${i+1} of ${this.pagesToImport.length})`;
|
|
const topX = Math.round(this.pageDimensions.width*this.importScale*column + this.gapSize*column);
|
|
const topY = Math.round(this.pageDimensions.height*this.importScale*row + this.gapSize*row);
|
|
|
|
ea.style.strokeColor = this.borderBox ? "#000000" : "transparent";
|
|
const boxID = ea.addRect(
|
|
topX,
|
|
topY,
|
|
imgWidth,
|
|
imgHeight
|
|
);
|
|
const boxEl = ea.getElement(boxID) as any;
|
|
if(this.lockAfterImport) boxEl.locked = true;
|
|
|
|
const imageID = await ea.addImage(
|
|
topX,
|
|
topY,
|
|
this.pdfFile.path + `#page=${page}`,
|
|
false,
|
|
false);
|
|
const imgEl = ea.getElement(imageID) as any;
|
|
imgEl.width = imgWidth;
|
|
imgEl.height = imgHeight;
|
|
if(this.lockAfterImport) imgEl.locked = true;
|
|
|
|
ea.addToGroup([boxID,imageID]);
|
|
|
|
if(this.frame) {
|
|
const frameID = ea.addFrame(topX, topY,imgWidth,imgHeight,`${page}`);
|
|
ea.addElementsToFrame(frameID, [boxID,imageID]);
|
|
ea.getElement(frameID).link = this.pdfFile.path + `#page=${page}`;
|
|
}
|
|
|
|
switch(this.direction) {
|
|
case "right":
|
|
column = (column + 1) % this.numColumns;
|
|
if(column === 0) row++;
|
|
break;
|
|
case "down":
|
|
row = (row + 1) % this.numRows;
|
|
if(row === 0) column++;
|
|
break;
|
|
}
|
|
}
|
|
if(this.groupPages) {
|
|
const ids = ea.getElements()
|
|
.filter(el=>!this.frame || (el.type === "frame"))
|
|
.map(el => el.id);
|
|
ea.addToGroup(ids);
|
|
}
|
|
await ea.addElementsToView(true,true,false);
|
|
const api = ea.getExcalidrawAPI() as ExcalidrawImperativeAPI;
|
|
const ids = ea.getElements().map(el => el.id);
|
|
const viewElements = ea.getViewElements().filter(el => ids.includes(el.id));
|
|
api.selectElements(viewElements);
|
|
api.zoomToFit(viewElements);
|
|
ea.destroy();
|
|
this.close();
|
|
})
|
|
importButton = button;
|
|
importButton.buttonEl.style.display = "none";
|
|
});
|
|
importMessage = actionButton.descEl;
|
|
importMessage.addClass("mod-warning");
|
|
if(this.pdfFile) {
|
|
search.setValue(this.pdfFile.path);
|
|
await setFile(this.pdfFile); //on drop if opened with a file
|
|
suggester.close();
|
|
pageRangesTextComponent.inputEl.focus();
|
|
} else {
|
|
search.inputEl.focus();
|
|
}
|
|
|
|
importButtonMessages();
|
|
}
|
|
}
|
|
|