mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
2.4.0-beta-10
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
(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<ExcalidrawElement>[];
|
||||
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<ExcalidrawTextElement, [string, TFile]>();
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = (() => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
2
src/types/types.d.ts
vendored
2
src/types/types.d.ts
vendored
@@ -22,7 +22,7 @@ export type DeviceType = {
|
||||
isMacOS: boolean,
|
||||
isWindows: boolean,
|
||||
isIOS: boolean,
|
||||
isAndroid: boolean
|
||||
isAndroid: boolean,
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -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<string> {
|
||||
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<string> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<string> {
|
||||
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<string> {
|
||||
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));
|
||||
}
|
||||
|
||||
100
src/workers/compression-worker.ts
Normal file
100
src/workers/compression-worker.ts
Normal file
@@ -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<r.length;n++)e[r][r.charAt(n)]=n}return e[r][o]}var i={compressToBase64:function(r){if(null==r)return"";var n=i._compress(r,6,function(r){return o.charAt(r)});switch(n.length%4){default:case 0:return n;case 1:return n+"===";case 2:return n+"==";case 3:return n+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(n){return t(o,r.charAt(n))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(r){return null==r?"":""==r?null:i._decompress(r.length,16384,function(o){return r.charCodeAt(o)-32})},compressToUint8Array:function(r){for(var o=i.compress(r),n=new Uint8Array(2*o.length),e=0,t=o.length;e<t;e++){var s=o.charCodeAt(e);n[2*e]=s>>>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<t;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(r){return null==r?"":i._compress(r,6,function(r){return n.charAt(r)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(o){return t(n,r.charAt(o))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(r,o,n){if(null==r)return"";var e,t,i,s={},u={},a="",p="",c="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<r.length;i+=1)if(a=r.charAt(i),Object.prototype.hasOwnProperty.call(s,a)||(s[a]=f++,u[a]=!0),p=c+a,Object.prototype.hasOwnProperty.call(s,p))c=p;else{if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=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<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=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<string> {
|
||||
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();
|
||||
@@ -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<void>;
|
||||
view: ExcalidrawView;
|
||||
forcesave: boolean;
|
||||
preventReload: boolean;
|
||||
}
|
||||
|
||||
onmessage = async function (e: MessageEvent<SaveWorkerMessageData>) {
|
||||
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();
|
||||
}
|
||||
|
||||
*/
|
||||
Reference in New Issue
Block a user