From ffdb05429119cf56869bf58f62fb14804e7bb44d Mon Sep 17 00:00:00 2001 From: zsviczian Date: Mon, 26 Aug 2024 22:40:45 +0200 Subject: [PATCH] 2.4.0-beta-10 --- manifest-beta.json | 2 +- package.json | 1 - rollup.config.js | 2 - src/ExcalidrawAutomate.ts | 4 +- src/ExcalidrawData.ts | 39 +++++++- src/ExcalidrawView.ts | 118 ++++++++++++++++++++--- src/constants/constants.ts | 2 +- src/main.ts | 3 + src/types/types.d.ts | 2 +- src/utils/ExcalidrawViewUtils.ts | 17 ++-- src/utils/Utils.ts | 16 +++- src/workers/compression-worker.ts | 100 +++++++++++++++++++ src/workers/save-worker.ts | 153 ------------------------------ 13 files changed, 274 insertions(+), 185 deletions(-) create mode 100644 src/workers/compression-worker.ts delete mode 100644 src/workers/save-worker.ts diff --git a/manifest-beta.json b/manifest-beta.json index 8015fca..f902dae 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "obsidian-excalidraw-plugin", "name": "Excalidraw", - "version": "2.4.0-beta-9", + "version": "2.4.0-beta-10", "minAppVersion": "1.1.6", "description": "An Obsidian plugin to edit and view Excalidraw drawings", "author": "Zsolt Viczian", diff --git a/package.json b/package.json index f1890ce..3648b2b 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,6 @@ "rollup-plugin-postprocess": "github:brettz9/rollup-plugin-postprocess#update", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.34.1", - "rollup-plugin-web-worker-loader": "^1.6.1", "tslib": "^2.6.1", "ttypescript": "^1.5.15", "typescript": "^5.2.2", diff --git a/rollup.config.js b/rollup.config.js index 9d28dcd..3942ae5 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -4,7 +4,6 @@ import replace from "@rollup/plugin-replace"; import { terser } from "rollup-plugin-terser"; import copy from "rollup-plugin-copy"; import typescript2 from "rollup-plugin-typescript2"; -//import webWorker from "rollup-plugin-web-worker-loader"; import fs from 'fs'; import LZString from 'lz-string'; import postprocess from 'rollup-plugin-postprocess'; @@ -100,7 +99,6 @@ const BUILD_CONFIG = { }, plugins: getRollupPlugins( {tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json"}, - //webWorker({ inline: true, forceInline: true, targetPlatform: "browser" }), ...(isProd ? [ terser({ toplevel: false, compress: { passes: 2 } }), //!postprocess - the version available on npmjs does not work, need this update: diff --git a/src/ExcalidrawAutomate.ts b/src/ExcalidrawAutomate.ts index 1240061..d1a4eb2 100644 --- a/src/ExcalidrawAutomate.ts +++ b/src/ExcalidrawAutomate.ts @@ -278,11 +278,11 @@ export class ExcalidrawAutomate { return getNewUniqueFilepath(app.vault, filename, folderAndPath.folder); } - public compressToBase64(str:string):string { + public compressToBase64(str:string): string { return LZString.compressToBase64(str); } - public decompressFromBase64(str:string):string { + public decompressFromBase64(str:string): string { return LZString.decompressFromBase64(str); } diff --git a/src/ExcalidrawData.ts b/src/ExcalidrawData.ts index c66af6f..9c97a13 100644 --- a/src/ExcalidrawData.ts +++ b/src/ExcalidrawData.ts @@ -35,6 +35,7 @@ import { updateFrontmatterInString, wrapTextAtCharLength, arrayToMap, + compressAsync, } from "./utils/Utils"; import { cleanBlockRef, cleanSectionHeading, getAttachmentsFolderAndFilePath, isObsidianThemeDark } from "./utils/ObsidianUtils"; import { @@ -219,10 +220,22 @@ export function getJSON(data: string): { scene: string; pos: number } { return { scene: data, pos: parts.value ? parts.value.index : 0 }; } -export function getMarkdownDrawingSection( +export async function getMarkdownDrawingSectionAsync ( jsonString: string, compressed: boolean, ) { + const result = compressed + ? `## Drawing\n\x60\x60\x60compressed-json\n${await compressAsync( + jsonString, + )}\n\x60\x60\x60\n%%` + : `## Drawing\n\x60\x60\x60json\n${jsonString}\n\x60\x60\x60\n%%`; + return result; +} + +export function getMarkdownDrawingSection( + jsonString: string, + compressed: boolean, +): string { const result = compressed ? `## Drawing\n\x60\x60\x60compressed-json\n${compress( jsonString, @@ -1395,7 +1408,7 @@ export class ExcalidrawData { * @returns markdown string */ disableCompression: boolean = false; - generateMD(deletedElements: ExcalidrawElement[] = []): string { + generateMDBase(deletedElements: ExcalidrawElement[] = []) { let outString = this.textElementCommentedOut ? "%%\n" : ""; outString += `# Excalidraw Data\n## Text Elements\n`; if (this.plugin.settings.addDummyTextElement) { @@ -1462,13 +1475,31 @@ export class ExcalidrawData { appState: this.scene.appState, files: this.scene.files }, null, "\t"); + return { outString, sceneJSONstring }; + } + + async generateMDAsync(deletedElements: ExcalidrawElement[] = []): Promise { + const { outString, sceneJSONstring } = this.generateMDBase(deletedElements); const result = ( outString + (this.textElementCommentedOut ? "" : "%%\n") + - getMarkdownDrawingSection( + (await getMarkdownDrawingSectionAsync( sceneJSONstring, this.disableCompression ? false : this.plugin.settings.compress, - ) + )) + ); + return result; + } + + generateMDSync(deletedElements: ExcalidrawElement[] = []): string { + const { outString, sceneJSONstring } = this.generateMDBase(deletedElements); + const result = ( + outString + + (this.textElementCommentedOut ? "" : "%%\n") + + (getMarkdownDrawingSection( + sceneJSONstring, + this.disableCompression ? false : this.plugin.settings.compress, + )) ); return result; } diff --git a/src/ExcalidrawView.ts b/src/ExcalidrawView.ts index dba01df..08c8963 100644 --- a/src/ExcalidrawView.ts +++ b/src/ExcalidrawView.ts @@ -144,6 +144,7 @@ import { SelectCard } from "./dialogs/SelectCard"; import { Packages } from "./types/types"; import React from "react"; import { diagramToHTML } from "./utils/matic"; +import { IS_WORKER_SUPPORTED } from "./workers/compression-worker"; const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000; const PREVENT_RELOAD_TIMEOUT = 2000; @@ -765,6 +766,7 @@ export default class ExcalidrawView extends TextFileView { //added this to avoid Electron crash when terminating a popout window and saving the drawing, need to check back //can likely be removed once this is resolved: https://github.com/electron/electron/issues/40607 if(this.semaphores?.viewunload) { + await this.prepareGetViewData(); const d = this.getViewData(); const plugin = this.plugin; const file = this.file; @@ -775,6 +777,7 @@ export default class ExcalidrawView extends TextFileView { return; } + await this.prepareGetViewData(); await super.save(); if (process.env.NODE_ENV === 'development') { if (DEBUGGING) { @@ -840,16 +843,23 @@ export default class ExcalidrawView extends TextFileView { // get the new file content // if drawing is in Text Element Edit Lock, then everything should be parsed and in sync // if drawing is in Text Element Edit Unlock, then everything is raw and parse and so an async function is not required here - - getViewData() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getViewData, "ExcalidrawView.getViewData"); + /** + * I moved the logic from getViewData to prepareGetViewData because getViewData is Sync and prepareGetViewData is async + * prepareGetViewData is async because of moving compression to a worker thread in 2.4.0 + */ + private viewSaveData: string = ""; + + async prepareGetViewData(): Promise { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.prepareGetViewData, "ExcalidrawView.prepareGetViewData"); if (!this.excalidrawAPI || !this.excalidrawData.loaded) { - return this.data; + this.viewSaveData = this.data; + return; } const scene = this.getScene(); if(!scene) { - return this.data; + this.viewSaveData = this.data; + return; } //include deleted elements in save in case saving in markdown mode @@ -881,17 +891,29 @@ export default class ExcalidrawView extends TextFileView { this.excalidrawData.disableCompression = this.plugin.settings.decompressForMDView && this.isEditedAsMarkdownInOtherView(); } - const result = header + this.excalidrawData.generateMD( - this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) //will be concatenated to scene.elements - ) + tail; + const result = IS_WORKER_SUPPORTED + ? (header + (await this.excalidrawData.generateMDAsync( + this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) //will be concatenated to scene.elements + )) + tail) + : (header + (this.excalidrawData.generateMDSync( + this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) //will be concatenated to scene.elements + )) + tail) + this.excalidrawData.disableCompression = false; - return result; + this.viewSaveData = result; + return; } if (this.compatibilityMode) { - return JSON.stringify(scene, null, "\t"); + this.viewSaveData = JSON.stringify(scene, null, "\t"); + return; } - return this.data; + this.viewSaveData = this.data; + return; + } + + getViewData() { + return this.viewSaveData ?? this.data; } private hiddenMobileLeaves:[WorkspaceLeaf,string][] = []; @@ -2097,6 +2119,7 @@ export default class ExcalidrawView extends TextFileView { // clear the view content clear() { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clear, "ExcalidrawView.clear"); + this.viewSaveData = ""; this.canvasNodeFactory.purgeNodes(); this.embeddableRefs.clear(); this.embeddableLeafRefs.clear(); @@ -3787,6 +3810,7 @@ export default class ExcalidrawView extends TextFileView { private onPaste (data: ClipboardData, event: ClipboardEvent | null) { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onPaste, "ExcalidrawView.onPaste", data, event); + const api = this.excalidrawAPI as ExcalidrawImperativeAPI; const ea = this.getHookServer(); if(data && ea.onPasteHook) { const res = ea.onPasteHook({ @@ -3849,7 +3873,6 @@ export default class ExcalidrawView extends TextFileView { const quoteWithRef = obsidianPDFQuoteWithRef(data.text); if(quoteWithRef) { const ea = getEA(this) as ExcalidrawAutomate; - const api = this.excalidrawAPI as ExcalidrawImperativeAPI; const st = api.getAppState(); const strokeC = st.currentItemStrokeColor; const viewC = st.viewBackgroundColor; @@ -3877,6 +3900,77 @@ export default class ExcalidrawView extends TextFileView { if (data.elements) { window.setTimeout(() => this.save(), 30); //removed prevent reload = false, as reload was triggered when pasted containers were processed and there was a conflict with the new elements } + + //process pasted text after it was processed into elements by Excalidraw + //I let Excalidraw handle the paste first, e.g. to split text by lines + //Only process text if it includes links or embeds that need to be parsed + if(data && data.text && data.text.match(/(\[\[[^\]]*]])|(\[[^\]]*]\([^)]*\))/gm)) { + const prevElements = api.getSceneElements().filter(el=>el.type === "text").map(el=>el.id); + + window.setTimeout(async ()=>{ + const sceneElements = api.getSceneElementsIncludingDeleted() as Mutable[]; + const newElements = sceneElements.filter(el=>el.type === "text" && !el.isDeleted && !prevElements.includes(el.id)) as ExcalidrawTextElement[]; + + //collect would-be image elements and their corresponding files and links + const imageElementsMap = new Map(); + let element: ExcalidrawTextElement; + const callback = (link: string, file: TFile) => { + imageElementsMap.set(element, [link, file]); + } + newElements.forEach((el:ExcalidrawTextElement)=>{ + element = el; + isTextImageTransclusion(el.originalText,this,callback); + }); + + //if there are no image elements, save and return + //Save will ensure links and embeds are parsed + if(imageElementsMap.size === 0) { + this.save(false); //saving because there still may be text transclusions + return; + }; + + //if there are image elements + //first delete corresponding "old" text elements + for(const [el, [link, file]] of imageElementsMap) { + const clone = cloneElement(el); + clone.isDeleted = true; + this.excalidrawData.deleteTextElement(clone.id); + sceneElements[sceneElements.indexOf(el)] = clone; + } + this.updateScene({elements: sceneElements, storeAction: "update"}); + + //then insert images and embeds + //shift text elements down to make space for images and embeds + const ea:ExcalidrawAutomate = getEA(this); + let offset = 0; + for(const el of newElements) { + const topleft = {x: el.x, y: el.y+offset}; + if(imageElementsMap.has(el)) { + const [link, file] = imageElementsMap.get(el); + if(IMAGE_TYPES.contains(file.extension)) { + const id = await insertImageToView (ea, topleft, file, undefined, false); + offset += ea.getElement(id).height - el.height; + } else if(file.extension !== "pdf") { + //isTextImageTransclusion will not return text only markdowns, this is here + //for the future when we may want to support other embeddables + const id = await insertEmbeddableToView (ea, topleft, file, link, false); + offset += ea.getElement(id).height - el.height; + } else { + const modal = new UniversalInsertFileModal(this.plugin, this); + modal.open(file, topleft); + } + } else { + if(offset !== 0) { + ea.copyViewElementsToEAforEditing([el]); + ea.getElement(el.id).y = topleft.y; + } + } + } + await ea.addElementsToView(false,true); + ea.selectElementsInView(newElements.map(el=>el.id)); + ea.destroy(); + },200) //parse transclusion and links after paste + } return true; } diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 962aa5b..df28820 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -121,7 +121,7 @@ export const DEVICE: DeviceType = { isMacOS: document.body.hasClass("mod-macos") && ! document.body.hasClass("is-ios"), isWindows: document.body.hasClass("mod-windows"), isIOS: document.body.hasClass("is-ios"), - isAndroid: document.body.hasClass("is-android") + isAndroid: document.body.hasClass("is-android"), }; export const ROOTELEMENTSIZE = (() => { diff --git a/src/main.ts b/src/main.ts index b31fa6c..b7bd87d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -138,6 +138,7 @@ import { showFrameSettings } from "./dialogs/FrameSettings"; import { ExcalidrawLib } from "./ExcalidrawLib"; import { Rank, SwordColors } from "./menu/ActionIcons"; import { RankMessage } from "./dialogs/RankMessage"; +import { initCompressionWorker, terminateCompressionWorker } from "./workers/compression-worker"; declare let EXCALIDRAW_PACKAGES:string; declare let react:any; @@ -311,6 +312,7 @@ export default class ExcalidrawPlugin extends Plugin { }*/ async onload() { + initCompressionWorker(); this.loadTimestamp = Date.now(); addIcon(ICON_NAME, EXCALIDRAW_ICON); addIcon(SCRIPTENGINE_ICON_NAME, SCRIPTENGINE_ICON); @@ -3316,6 +3318,7 @@ export default class ExcalidrawPlugin extends Plugin { react = null; reactDOM = null; excalidrawLib = null; + terminateCompressionWorker(); } public async embedDrawing(file: TFile) { diff --git a/src/types/types.d.ts b/src/types/types.d.ts index fa88245..45bf829 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -22,7 +22,7 @@ export type DeviceType = { isMacOS: boolean, isWindows: boolean, isIOS: boolean, - isAndroid: boolean + isAndroid: boolean, }; declare global { diff --git a/src/utils/ExcalidrawViewUtils.ts b/src/utils/ExcalidrawViewUtils.ts index f52d121..c4b43d1 100644 --- a/src/utils/ExcalidrawViewUtils.ts +++ b/src/utils/ExcalidrawViewUtils.ts @@ -5,7 +5,7 @@ import { ExcalidrawAutomate } from "src/ExcalidrawAutomate"; import { REGEX_LINK, REG_LINKINDEX_HYPERLINK, getExcalidrawMarkdownHeaderSection, REGEX_TAGS } from "src/ExcalidrawData"; import ExcalidrawView from "src/ExcalidrawView"; import { ExcalidrawElement, ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types"; -import { getLinkParts } from "./Utils"; +import { getEmbeddedFilenameParts, getLinkParts, isImagePartRef } from "./Utils"; import { cleanSectionHeading } from "./ObsidianUtils"; import { getEA } from "src"; import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types"; @@ -18,8 +18,9 @@ export async function insertImageToView( position: { x: number, y: number }, file: TFile | string, scale?: boolean, + shouldInsertToView: boolean = true, ):Promise { - ea.clear(); + if(shouldInsertToView) {ea.clear();} ea.style.strokeColor = "transparent"; ea.style.backgroundColor = "transparent"; const api = ea.getExcalidrawAPI(); @@ -30,7 +31,7 @@ export async function insertImageToView( file, scale, ); - await ea.addElementsToView(false, true, true); + if(shouldInsertToView) {await ea.addElementsToView(false, true, true);} return id; } @@ -39,12 +40,13 @@ export async function insertEmbeddableToView ( position: { x: number, y: number }, file?: TFile, link?: string, + shouldInsertToView: boolean = true, ):Promise { - ea.clear(); + if(shouldInsertToView) {ea.clear();} ea.style.strokeColor = "transparent"; ea.style.backgroundColor = "transparent"; if(file && (IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file)) && !ANIMATED_IMAGE_TYPES.contains(file.extension)) { - return await insertImageToView(ea, position, link??file); + return await insertImageToView(ea, position, link??file, undefined, shouldInsertToView); } else { const id = ea.addEmbeddable( position.x, @@ -54,7 +56,7 @@ export async function insertEmbeddableToView ( link, file, ); - await ea.addElementsToView(false, true, true); + if(shouldInsertToView) {await ea.addElementsToView(false, true, true);} return id; } } @@ -369,6 +371,9 @@ export function isTextImageTransclusion ( const link = match.value[1] ?? match.value[2]; const file = view.app.metadataCache.getFirstLinkpathDest(link?.split("#")[0], view.file.path); if(view.file === file) { + if(link?.split("#")[1] && !isImagePartRef(getEmbeddedFilenameParts(link))) { + return false; + } new Notice(t("RECURSIVE_INSERT_ERROR")); return false; } diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index d37f1ad..f631837 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -28,6 +28,7 @@ import { cleanBlockRef, cleanSectionHeading, getFileCSSClasses } from "./Obsidia import { updateElementLinksToObsidianLinks } from "src/ExcalidrawAutomate"; import { CropImage } from "./CropImage"; import opentype from 'opentype.js'; +import { runCompressionWorker } from "src/workers/compression-worker"; declare const PLUGIN_VERSION:string; declare var LZString: any; @@ -528,9 +529,12 @@ export function getLinkParts (fname: string, file?: TFile): LinkParts { }; }; +export async function compressAsync (data: string): Promise { + return await runCompressionWorker(data, "compress"); +} + export function compress (data: string): string { const compressed = LZString.compressToBase64(data); - let result = ''; const chunkSize = 256; for (let i = 0; i < compressed.length; i += chunkSize) { @@ -540,7 +544,11 @@ export function compress (data: string): string { return result.trim(); }; -export function decompress (data: string): string { +export async function decompressAsync (data: string): Promise { + return await runCompressionWorker(data, "decompress"); +}; + +export function decompress (data: string, isAsync:boolean = false): string { let cleanedData = ''; const length = data.length; @@ -765,6 +773,10 @@ export function getEmbeddedFilenameParts (fname:string): FILENAMEPARTS { } } +export function isImagePartRef (parts: FILENAMEPARTS): boolean { + return (parts.hasGroupref || parts.hasArearef || parts.hasFrameref || parts.hasClippedFrameref); +} + export function fragWithHTML (html: string) { return createFragment((frag) => (frag.createDiv().innerHTML = html)); } diff --git a/src/workers/compression-worker.ts b/src/workers/compression-worker.ts new file mode 100644 index 0000000..bcdcf96 --- /dev/null +++ b/src/workers/compression-worker.ts @@ -0,0 +1,100 @@ +function createWorkerBlob(jsCode:string) { + // Create a new Blob with the JavaScript code + const blob = new Blob([jsCode], { type: 'text/javascript' }); + + // Create a URL for the Blob + const url = URL.createObjectURL(blob); + + return url; +} + +const workerCode = ` +var LZString=function(){var r=String.fromCharCode,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",e={};function t(r,o){if(!e[r]){e[r]={};for(var n=0;n>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null==o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;e>=1}else{for(t=1,e=0;e>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e>=1;0==--l&&(l=Math.pow(2,h),h++),s[p]=f++,c=String(a)}if(""!==c){if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e>=1}else{for(t=1,e=0;e>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e>=1;0==--l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;e>=1;for(;;){if(m<<=1,v==o-1){d.push(n(m));break}v++}return d.join("")},decompress:function(r){return null==r?"":""==r?null:i._decompress(r.length,32768,function(o){return r.charCodeAt(o)})},_decompress:function(o,n,e){var t,i,s,u,a,p,c,l=[],f=4,h=4,d=3,m="",v=[],g={val:e(0),position:n,index:1};for(t=0;t<3;t+=1)l[t]=t;for(s=0,a=Math.pow(2,2),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 2:return""}for(l[3]=c,i=c,v.push(c);;){if(g.index>o)return"";for(s=0,a=Math.pow(2,d),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(c=s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 2:return v.join("")}if(0==f&&(f=Math.pow(2,d),d++),l[c])m=l[c];else{if(c!==h)return null;m=i+i.charAt(0)}v.push(m),l[h++]=i+m.charAt(0),i=m,0==--f&&(f=Math.pow(2,d),d++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module?module.exports=LZString:"undefined"!=typeof angular&&null!=angular&&angular.module("LZString",[]).factory("LZString",function(){return LZString}); +self.onmessage = function(e) { + const { data, action } = e.data; + try { + switch (action) { + case 'compress': + if (!data) throw new Error("No input string provided for compression."); + const compressed = LZString.compressToBase64(data); + let result = ''; + const chunkSize = 256; + for (let i = 0; i < compressed.length; i += chunkSize) { + result += compressed.slice(i, i + chunkSize) + '\\n\\n'; + } + self.postMessage({ compressed: result.trim() }); + break; + case 'decompress': + if (!data) throw new Error("No input string provided for decompression."); + let cleanedData = ''; + const length = data.length; + for (let i = 0; i < length; i++) { + const char = data[i]; + if (char !== '\\n' && char !== '\\r') { + cleanedData += char; + } + } + const decompressed = LZString.decompressFromBase64(cleanedData); + self.postMessage({ decompressed }); + break; + default: + throw new Error("Unknown action."); + } + } catch (error) { + // Post the error message back to the main thread + self.postMessage({ error: error.message }); + } +}; +`; + +let worker:Worker | null = null; + +export function initCompressionWorker() { + if(!worker) { + worker = new Worker(createWorkerBlob(workerCode)); + } +} + +export async function runCompressionWorker(data:string, action: 'compress' | 'decompress'): Promise { + return new Promise((resolve, reject) => { + worker.onmessage = function(e) { + const { compressed, decompressed, error } = e.data; + + if (error) { + reject(new Error(error)); + } else if (compressed || decompressed) { + resolve(compressed || decompressed); + } else { + reject(new Error('Unexpected response from worker')); + } + }; + + // Set up the worker's error handler + worker.onerror = function(error) { + reject(new Error(error.message)); + }; + + // Post the message to the worker + worker.postMessage({ data, action }); + }); +} + +export function terminateCompressionWorker() { + worker.terminate(); + worker = null; +} + +export let IS_WORKER_SUPPORTED = false; +function canCreateWorkerFromBlob() { + try { + const blob = new Blob(["self.onmessage = function() {}"]); + const url = URL.createObjectURL(blob); + const worker = new Worker(url); + worker.terminate(); + URL.revokeObjectURL(url); + IS_WORKER_SUPPORTED = true; + } catch (e) { + IS_WORKER_SUPPORTED = false; + } +} +canCreateWorkerFromBlob(); diff --git a/src/workers/save-worker.ts b/src/workers/save-worker.ts deleted file mode 100644 index be58ec4..0000000 --- a/src/workers/save-worker.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types"; -import ExcalidrawView from "src/ExcalidrawView"; -import { debug, DEBUGGING, ts } from "src/utils/DebugHelper"; -import { imageCache } from "src/utils/ImageCache"; - -interface SaveWorkerMessageData { - superSave: () => Promise; - view: ExcalidrawView; - forcesave: boolean; - preventReload: boolean; -} - -onmessage = async function (e: MessageEvent) { - const { superSave, view, forcesave, preventReload } = e.data; - - try { - (process.env.NODE_ENV === 'development') && ts("allow save",1); - const scene = view.getScene(); - - if (view.compatibilityMode) { - await view.excalidrawData.syncElements(scene); - } else if ( - await view.excalidrawData.syncElements(scene, view.excalidrawAPI.getAppState().selectedElementIds) - && !view.semaphores.popoutUnload //Obsidian going black after REACT 18 migration when closing last leaf on popout - ) { - (process.env.NODE_ENV === 'development') && ts("ExcalidrawView.beforeLoadDrawing",1); - await view.loadDrawing( - false, - view.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) - ); - (process.env.NODE_ENV === 'development') && ts("ExcalidrawView.afterLoadDrawing",1); - } - (process.env.NODE_ENV === 'development') && ts("after sync elements",1); - - //reload() is triggered indirectly when saving by the modifyEventHandler in main.ts - //prevent reload is set here to override reload when not wanted: typically when the user is editing - //and we do not want to interrupt the flow by reloading the drawing into the canvas. - view.clearDirty(); - view.clearPreventReloadTimer(); - - view.semaphores.preventReload = preventReload; - //added this to avoid Electron crash when terminating a popout window and saving the drawing, need to check back - //can likely be removed once this is resolved: https://github.com/electron/electron/issues/40607 - if(view.semaphores?.viewunload) { - const d = view.getViewData(); - const plugin = view.plugin; - const file = view.file; - window.setTimeout(async ()=>{ - await plugin.app.vault.modify(file,d); - await imageCache.addBAKToCache(file.path,d); - },200) - return; - } - - (process.env.NODE_ENV === 'development') && ts("before super.save",1); - await superSave(); - (process.env.NODE_ENV === 'development') && ts("after super.save",1); - if (process.env.NODE_ENV === 'development') { - if (DEBUGGING) { - debug(self.onmessage, `ExcalidrawView.save, super.save finished`, view.file); - console.trace(); - } - } - //saving to backup with a delay in case application closes in the meantime, I want to avoid both save and backup corrupted. - const path = view.file.path; - //@ts-ignore - const data = view.lastSavedData; - window.setTimeout(()=>imageCache.addBAKToCache(path,data),50); - const triggerReload = (view.lastSaveTimestamp === view.file.stat.mtime) && - !preventReload && forcesave; - view.lastSaveTimestamp = view.file.stat.mtime; - //view.clearDirty(); //moved to right after allow save, to avoid autosave collision with load drawing - - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/629 - //there were odd cases when preventReload semaphore did not get cleared and consequently a synchronized image - //did not update the open drawing - if(preventReload) { - view.setPreventReload(); - } - - postMessage({ success: true, triggerReload }); - } catch (error) { - postMessage({ success: false, error: (error as Error).message }); - } -}; - - -/* ExcalidrawView - -import SaveWorker from "web-worker:./workers/save-worker.ts"; - -//... - - try { - if (allowSave) { - const worker = new SaveWorker({name: "Excalidraw File Save Worker"}); - const promise = new Promise<{success:boolean, error?:Error, triggerReload?: boolean}>((resolve, reject) => { - worker.onmessage = (e: MessageEvent<{ success: boolean; error?: Error; triggerReload?: boolean }>) => { - resolve(e.data); - }; - worker.onerror = (e: ErrorEvent) => { - reject(new Error(e.message)); - }; - worker.postMessage({ - superSave: super.save.bind(this), - view: this, - preventReload, - forcesave, - }); - }); - const { success, error, triggerReload: tr } = await promise; - worker.terminate(); - if(error) { - throw error; - } - if(typeof tr !== "undefined") { - triggerReload = tr; - } - } - - // !triggerReload means file has not changed. No need to re-export - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1209 (added popout unload to the condition) - if (!triggerReload && !this.semaphores.autosaving && (!this.semaphores.viewunload || this.semaphores.popoutUnload)) { - const autoexportPreference = this.excalidrawData.autoexportPreference; - if ( - (autoexportPreference === AutoexportPreference.inherit && this.plugin.settings.autoexportSVG) || - autoexportPreference === AutoexportPreference.both || autoexportPreference === AutoexportPreference.svg - ) { - this.saveSVG(); - } - if ( - (autoexportPreference === AutoexportPreference.inherit && this.plugin.settings.autoexportPNG) || - autoexportPreference === AutoexportPreference.both || autoexportPreference === AutoexportPreference.png - ) { - this.savePNG(); - } - if ( - !this.compatibilityMode && - this.plugin.settings.autoexportExcalidraw - ) { - this.saveExcalidraw(); - } - } - } catch (e) { - errorlog({ - where: "ExcalidrawView.save", - fn: this.save, - error: e, - }); - warningUnknowSeriousError(); - } - -*/ \ No newline at end of file