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"; import { UniversalInsertFileModal } from "src/dialogs/UniversalInsertFileModal"; import { DEBUGGING, debug } from "src/utils/DebugHelper"; declare const PLUGIN_VERSION:string; const dark = '; centerPointer: Function; observer: WeakRef; }; export type PanelState = { visible: boolean; top: number; left: number; theme: "dark" | "light"; excalidrawViewMode: boolean; minimized: boolean; isDirty: boolean; isFullscreen: boolean; isPreviewMode: boolean; scriptIconMap: ScriptIconMap | null; }; const TOOLS_PANEL_WIDTH = 228; export class ToolsPanel extends React.Component { 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; private view: ExcalidrawView; componentWillUnmount(): void { if (this.containerRef.current) { this.props.observer.deref()?.unobserve(this.containerRef.current); } this.setState({ scriptIconMap: null }); this.containerRef = null; this.view = null; } constructor(props: PanelProps) { super(props); this.view = props.view.deref(); const react = this.view.packages.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) { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateScriptIconMap,"ToolsPanel.updateScriptIconMap()"); this.setState(() => { return { scriptIconMap }; }); } setPreviewMode(isPreviewMode: boolean) { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setPreviewMode,"ToolsPanel.setPreviewMode()"); this.setState(() => { return { isPreviewMode, }; }); } setFullscreen(isFullscreen: boolean) { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setFullscreen,"ToolsPanel.setFullscreen()"); this.setState(() => { return { isFullscreen, }; }); } setDirty(isDirty: boolean) { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setDirty,"ToolsPanel.setDirty()"); this.setState(()=> { return { isDirty, }; }); } setExcalidrawViewMode(isViewModeEnabled: boolean) { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setExcalidrawViewMode,"ToolsPanel.setExcalidrawViewMode()"); this.setState(() => { return { excalidrawViewMode: isViewModeEnabled, }; }); } toggleVisibility(isMobileOrZen: boolean) { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleVisibility,"ToolsPanel.toggleVisibility()"); this.setTopCenter(isMobileOrZen); this.setState((prevState: PanelState) => { return { visible: !prevState.visible, }; }); } setTheme(theme: "dark" | "light") { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setTheme,"ToolsPanel.setTheme()"); this.setState((prevState: PanelState) => { return { theme, }; }); } setTopCenter(isMobileOrZen: boolean) { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setTopCenter,"ToolsPanel.setTopCenter()"); 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) { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updatePosition,"ToolsPanel.updatePosition()"); 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, }; }); } actionOpenScriptInstallDialog() { new ScriptInstallPrompt(this.view.plugin).open(); } actionOpenReleaseNotes() { new ReleaseNotes( this.view.app, this.view.plugin, PLUGIN_VERSION, ).open(); } actionConvertExcalidrawToMD() { this.view.convertExcalidrawToMD(); } actionToggleViewMode() { if (this.state.isPreviewMode) { this.view.changeTextMode(TextMode.raw); } else { this.view.changeTextMode(TextMode.parsed); } } actionToggleTrayMode() { this.view.toggleTrayMode(); } actionToggleFullscreen() { if (this.state.isFullscreen) { this.view.exitFullscreen(); } else { this.view.gotoFullscreen(); } } actionSearch() { search(this.view); } actionOCR(e:React.MouseEvent) { if(!this.view.plugin.settings.taskboneEnabled) { new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000); return; } this.view.plugin.taskbone.getTextForView(this.view, {forceReScan: isWinCTRLorMacCMD(e)}); } actionOpenLink(e:React.MouseEvent) { 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.view.handleLinkClick(event); } actionOpenLinkProperties() { const event = new MouseEvent("click", { ctrlKey: true, metaKey: true, shiftKey: false, altKey: false, }); this.view.handleLinkClick(event); } actionForceSave() { this.view.forceSave(); } actionExportLibrary() { this.view.plugin.exportLibrary(); } actionExportImage() { const view = this.view; if(!view.exportDialog) { view.exportDialog = new ExportDialog(view.plugin, view,view.file); view.exportDialog.createForm(); } view.exportDialog.open(); } actionOpenAsMarkdown() { this.view.openAsMarkdown(); } actionLinkToElement(e:React.MouseEvent) { if(isWinALTorMacOPT(e)) { openExternalLink("https://youtu.be/yZQoJg2RCKI", this.view.app); return; } this.view.copyLinkToSelectedElementToClipboard( isWinCTRLorMacCMD(e) ? "group=" : (isSHIFT(e) ? "area=" : "") ); } actionAddAnyFile() { this.props.centerPointer(); const insertFileModal = new UniversalInsertFileModal(this.view.plugin, this.view); insertFileModal.open(); } actionInsertImage() { this.props.centerPointer(); this.view.plugin.insertImageDialog.start( this.view, ); } actionInsertPDF() { this.props.centerPointer(); const insertPDFModal = new InsertPDFModal(this.view.plugin, this.view); insertPDFModal.open(); } actionInsertMarkdown() { this.props.centerPointer(); this.view.plugin.insertMDDialog.start( this.view, ); } actionInsertBackOfNote() { this.props.centerPointer(); this.view.insertBackOfTheNoteCard(); } actionInsertLaTeX(e:React.MouseEvent) { if(isWinALTorMacOPT(e)) { openExternalLink("https://youtu.be/r08wk-58DPk", this.view.app); return; } this.props.centerPointer(); insertLaTeXToView(this.view); } actionInsertLink() { this.props.centerPointer(); this.view.plugin.insertLinkDialog.start( this.view.file.path, (text: string, fontFamily?: 1 | 2 | 3 | 4, save?: boolean) => this.view.addText (text, fontFamily, save), ); } actionImportSVG(e:React.MouseEvent) { this.view.plugin.importSVGDialog.start(this.view); } actionCropImage(e:React.MouseEvent) { // @ts-ignore this.view.app.commands.executeCommandById("obsidian-excalidraw-plugin:crop-image"); } async actionRunScript(key: string) { const view = this.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 ); } } async actionPinScript(key: string, scriptName: string) { const view = this.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: ${scriptName}`, duration: 3000, closable: true}); } else { plugin.settings.pinnedScripts.push(key); api?.setToast({message:`Pinned: ${scriptName}`, duration: 3000, closable: true}) } await plugin.saveSettings(); plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> { if (v.view instanceof ExcalidrawView) v.view.updatePinnedScripts() }) } private islandOnClick(event: React.MouseEvent) { 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, }; }); } private islandOnPointerDown(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.view.ownerDocument?.removeEventListener("pointerup", onPointerUp); this.view.ownerDocument?.removeEventListener("pointermove", onDrag); }; event.preventDefault(); this.penDownX = this.pos3 = event.clientX; this.penDownY = this.pos4 = event.clientY; this.view.ownerDocument.addEventListener("pointerup", onPointerUp); this.view.ownerDocument.addEventListener("pointermove", onDrag); }; render() { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.render,"ToolsPanel.render()"); return (
Utility actions
{this.state.isPreviewMode === null ? ( ) : ( )}
Export actions
Insert actions
{this.renderScriptButtons(false)} {this.renderScriptButtons(true)}
); } private renderScriptButtons(isDownloaded: boolean) { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.renderScriptButtons,"ToolsPanel.renderScriptButtons()"); if (Object.keys(this.state.scriptIconMap).length === 0) { return ""; } const downloadedScriptsRoot = `${this.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])=>( ))}
))} ); } }