import clsx from "clsx"; import { Notice, TFile } from "obsidian"; import * as React from "react"; import { ActionButton } from "./ActionButton"; import { ICONS, saveIcon, stringToSVG } from "./ActionIcons"; import { DEVICE, SCRIPT_INSTALL_FOLDER, VIEW_TYPE_EXCALIDRAW } from "../constants/constants"; import { insertLaTeXToView, search } from "../ExcalidrawAutomate"; import ExcalidrawView, { TextMode } from "../ExcalidrawView"; import { t } from "../lang/helpers"; import { ReleaseNotes } from "../dialogs/ReleaseNotes"; import { ScriptIconMap } from "../Scripts"; import { ScriptInstallPrompt } from "src/dialogs/ScriptInstallPrompt"; import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types"; import { isWinALTorMacOPT, isWinCTRLorMacCMD, isSHIFT } from "src/utils/ModifierkeyHelper"; import { InsertPDFModal } from "src/dialogs/InsertPDFModal"; import { ExportDialog } from "src/dialogs/ExportDialog"; import { openExternalLink } from "src/utils/ExcalidrawViewUtils"; declare const PLUGIN_VERSION:string; const dark = ' { pos1: number = 0; pos2: number = 0; pos3: number = 0; pos4: number = 0; penDownX: number = 0; penDownY: number = 0; previousWidth: number = 0; previousHeight: number = 0; onRightEdge: boolean = false; onBottomEdge: boolean = false; public containerRef: React.RefObject; constructor(props: PanelProps) { super(props); const react = props.view.plugin.getPackage(props.view.ownerWindow).react; this.containerRef = react.createRef(); this.state = { visible: props.visible, top: 50, left: 200, theme: "dark", excalidrawViewMode: false, minimized: false, isDirty: false, isFullscreen: false, isPreviewMode: true, scriptIconMap: {}, }; } updateScriptIconMap(scriptIconMap: ScriptIconMap) { this.setState(() => { return { scriptIconMap }; }); } setPreviewMode(isPreviewMode: boolean) { this.setState(() => { return { isPreviewMode, }; }); } setFullscreen(isFullscreen: boolean) { this.setState(() => { return { isFullscreen, }; }); } setDirty(isDirty: boolean) { this.setState(()=> { return { isDirty, }; }); } setExcalidrawViewMode(isViewModeEnabled: boolean) { this.setState(() => { return { excalidrawViewMode: isViewModeEnabled, }; }); } toggleVisibility(isMobileOrZen: boolean) { this.setTopCenter(isMobileOrZen); this.setState((prevState: PanelState) => { return { visible: !prevState.visible, }; }); } setTheme(theme: "dark" | "light") { this.setState((prevState: PanelState) => { return { theme, }; }); } setTopCenter(isMobileOrZen: boolean) { this.setState(() => { return { left: (this.containerRef.current.clientWidth - TOOLS_PANEL_WIDTH - (isMobileOrZen ? 0 : TOOLS_PANEL_WIDTH + 4)) / 2 + this.containerRef.current.parentElement.offsetLeft + (isMobileOrZen ? 0 : TOOLS_PANEL_WIDTH + 4), top: 64 + this.containerRef.current.parentElement.offsetTop, }; }); } updatePosition(deltaY: number = 0, deltaX: number = 0) { this.setState(() => { const { offsetTop, offsetLeft, clientWidth: width, clientHeight: height, } = this.containerRef.current.firstElementChild as HTMLElement; const top = offsetTop - deltaY; const left = offsetLeft - deltaX; const { clientWidth: parentWidth, clientHeight: parentHeight, offsetTop: parentOffsetTop, offsetLeft: parentOffsetLeft, } = this.containerRef.current.parentElement; this.previousHeight = parentHeight; this.previousWidth = parentWidth; this.onBottomEdge = top >= parentHeight - height + parentOffsetTop; this.onRightEdge = left >= parentWidth - width + parentOffsetLeft; return { top: top < parentOffsetTop ? parentOffsetTop : this.onBottomEdge ? parentHeight - height + parentOffsetTop : top, left: left < parentOffsetLeft ? parentOffsetLeft : this.onRightEdge ? parentWidth - width + parentOffsetLeft : left, }; }); } render() { return (
) => { event.preventDefault(); if ( Math.abs(this.penDownX - this.pos3) > 5 || Math.abs(this.penDownY - this.pos4) > 5 ) { return; } this.setState((prevState: PanelState) => { return { minimized: !prevState.minimized, }; }); }} onPointerDown={(event: React.PointerEvent) => { const onDrag = (e: PointerEvent) => { e.preventDefault(); this.pos1 = this.pos3 - e.clientX; this.pos2 = this.pos4 - e.clientY; this.pos3 = e.clientX; this.pos4 = e.clientY; this.updatePosition(this.pos2, this.pos1); }; const onPointerUp = () => { this.props.view.ownerDocument?.removeEventListener("pointerup", onPointerUp); this.props.view.ownerDocument?.removeEventListener("pointermove", onDrag); }; event.preventDefault(); this.penDownX = this.pos3 = event.clientX; this.penDownY = this.pos4 = event.clientY; this.props.view.ownerDocument.addEventListener("pointerup", onPointerUp); this.props.view.ownerDocument.addEventListener("pointermove", onDrag); }} >
Utility actions
{ new ScriptInstallPrompt(this.props.view.plugin).open(); }} icon={ICONS.scriptEngine} view={this.props.view} /> { new ReleaseNotes( this.props.view.app, this.props.view.plugin, PLUGIN_VERSION, ).open(); }} icon={ICONS.releaseNotes} view={this.props.view} /> {this.state.isPreviewMode === null ? ( { this.props.view.convertExcalidrawToMD(); }} icon={ICONS.convertFile} view={this.props.view} /> ) : ( { if (this.state.isPreviewMode) { this.props.view.changeTextMode(TextMode.raw); } else { this.props.view.changeTextMode(TextMode.parsed); } }} icon={ this.state.isPreviewMode ? ICONS.rawMode : ICONS.parsedMode } view={this.props.view} /> )} { this.props.view.toggleTrayMode(); }} icon={ICONS.trayMode} view={this.props.view} /> { if (this.state.isFullscreen) { this.props.view.exitFullscreen(); } else { this.props.view.gotoFullscreen(); } }} icon={ this.state.isFullscreen ? ICONS.exitFullScreen : ICONS.gotoFullScreen } view={this.props.view} /> { search(this.props.view); }} icon={ICONS.search} view={this.props.view} /> ) => { if(!this.props.view.plugin.settings.taskboneEnabled) { new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000); return; } this.props.view.plugin.taskbone.getTextForView(this.props.view, isWinCTRLorMacCMD(e)); }} icon={ICONS.ocr} view={this.props.view} /> { const event = new MouseEvent("click", { ctrlKey: e.ctrlKey || !(DEVICE.isIOS || DEVICE.isMacOS), metaKey: e.metaKey || (DEVICE.isIOS || DEVICE.isMacOS), shiftKey: e.shiftKey, altKey: e.altKey, }); this.props.view.handleLinkClick(event); }} icon={ICONS.openLink} view={this.props.view} /> { const event = new MouseEvent("click", { ctrlKey: true, metaKey: true, shiftKey: false, altKey: false, }); this.props.view.handleLinkClick(event); }} icon={ICONS.openLinkProperties} view={this.props.view} /> { this.props.view.forceSave(); }} icon={saveIcon(this.state.isDirty)} view={this.props.view} />
Export actions
{ this.props.view.plugin.exportLibrary(); }} icon={ICONS.exportLibrary} view={this.props.view} /> { const view = this.props.view; if(!view.exportDialog) { view.exportDialog = new ExportDialog(view.plugin, view,view.file); view.exportDialog.createForm(); } view.exportDialog.open(); }} icon={ICONS.ExportImage} view={this.props.view} /> { this.props.view.openAsMarkdown(); }} icon={ICONS.switchToMarkdown} view={this.props.view} />
Insert actions
{ this.props.centerPointer(); this.props.view.plugin.insertImageDialog.start( this.props.view, ); }} icon={ICONS.insertImage} view={this.props.view} /> { this.props.centerPointer(); const insertPDFModal = new InsertPDFModal(this.props.view.plugin, this.props.view); insertPDFModal.open(); }} icon={ICONS.insertPDF} view={this.props.view} /> { this.props.centerPointer(); this.props.view.plugin.insertMDDialog.start( this.props.view, ); }} icon={ICONS.insertMD} view={this.props.view} /> { if(isWinALTorMacOPT(e)) { openExternalLink("https://youtu.be/r08wk-58DPk", this.props.view.app); return; } this.props.centerPointer(); insertLaTeXToView(this.props.view); }} icon={ICONS.insertLaTeX} view={this.props.view} /> { this.props.centerPointer(); this.props.view.plugin.insertLinkDialog.start( this.props.view.file.path, (text: string, fontFamily?: 1 | 2 | 3 | 4, save?: boolean) => this.props.view.addText (text, fontFamily, save), ); }} icon={ICONS.insertLink} view={this.props.view} /> ) => { if(isWinALTorMacOPT(e)) { openExternalLink("https://youtu.be/yZQoJg2RCKI", this.props.view.app); return; } this.props.view.copyLinkToSelectedElementToClipboard( isWinCTRLorMacCMD(e) ? "group=" : (isSHIFT(e) ? "area=" : "") ); }} icon={ICONS.copyElementLink} view={this.props.view} /> ) => { this.props.view.plugin.importSVGDialog.start(this.props.view); }} icon={ICONS.importSVG} view={this.props.view} /> ) => { // @ts-ignore this.props.view.app.commands.executeCommandById("obsidian-excalidraw-plugin:crop-image") }} icon={ICONS.Crop} view={this.props.view} />
{this.renderScriptButtons(false)} {this.renderScriptButtons(true)}
); } private renderScriptButtons(isDownloaded: boolean) { if (Object.keys(this.state.scriptIconMap).length === 0) { return ""; } const downloadedScriptsRoot = `${this.props.view.plugin.settings.scriptFolderPath}/${SCRIPT_INSTALL_FOLDER}/`; const filterCondition = (key: string): boolean => isDownloaded ? key.startsWith(downloadedScriptsRoot) : !key.startsWith(downloadedScriptsRoot); if ( Object.keys(this.state.scriptIconMap).filter((k) => filterCondition(k)) .length === 0 ) { return ""; } const groups = new Set(); Object.keys(this.state.scriptIconMap) .filter((k) => filterCondition(k)) .forEach(k => groups.add(this.state.scriptIconMap[k].group)) const scriptlist = Array.from(groups).sort((a,b)=>a>b?1:-1); scriptlist.push(scriptlist.shift()); return ( <> {scriptlist.map((group, index) => (
{isDownloaded ? group : (group === "" ? "User" : "User/"+group)}
{Object.entries(this.state.scriptIconMap) .filter(([k,v])=>v.group === group) .sort() .map(([key,value])=>( { const view = this.props.view; const plugin = view.plugin; const f = app.vault.getAbstractFileByPath(key); if (f && f instanceof TFile) { plugin.scriptEngine.executeScript( view, await app.vault.read(f), plugin.scriptEngine.getScriptName(f), f ); } }} longpress={async () => { const view = this.props.view; const api = view.excalidrawAPI as ExcalidrawImperativeAPI; const plugin = view.plugin; await plugin.loadSettings(); const index = plugin.settings.pinnedScripts.indexOf(key) if(index > -1) { plugin.settings.pinnedScripts.splice(index,1); api?.setToast({message:`Pin removed: ${value.name}`, duration: 3000, closable: true}); } else { plugin.settings.pinnedScripts.push(key); api?.setToast({message:`Pinned: ${value.name}`, duration: 3000, closable: true}) } await plugin.saveSettings(); app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> { if (v.view instanceof ExcalidrawView) v.view.updatePinnedScripts() }) }} icon={ value.svgString ? stringToSVG(value.svgString) : ( ICONS.cog ) } view={this.props.view} /> ))}
))} ); } }