From f2801c9f1c4e48cc36e6ae29b0897332fcf34871 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Sun, 6 Apr 2025 12:21:25 +0200 Subject: [PATCH 01/24] Bug fix: save on no changes --- src/editorTab.ts | 14 ++++++++++++-- src/protyle.ts | 6 +++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/editorTab.ts b/src/editorTab.ts index 8ebc0a0..001320d 100644 --- a/src/editorTab.ts +++ b/src/editorTab.ts @@ -45,8 +45,18 @@ async function saveCallback(editor: Editor, fileID: string, oldSyncID: string, s try { newSyncID = (await uploadAsset(fileID, SVG_MIME, svgElem.outerHTML)).syncID; - await replaceSyncID(fileID, oldSyncID, newSyncID); - await removeFile(DATA_PATH + IDsToAssetPath(fileID, oldSyncID)); + if(newSyncID != oldSyncID) { + const changed = await replaceSyncID(fileID, oldSyncID, newSyncID); + if(!changed) { + alert( + "Error replacing old sync ID with new one! You may need to manually replace the file path." + + "\nTry saving the drawing again. This is a bug, please open an issue as soon as you can." + + "\nIf your document doesn't show the drawing, you can recover it from the SiYuan workspace directory." + ); + return oldSyncID; + } + await removeFile(DATA_PATH + IDsToAssetPath(fileID, oldSyncID)); + } saveButton.setDisabled(true); setTimeout(() => { // @todo improve save button feedback saveButton.setDisabled(false); diff --git a/src/protyle.ts b/src/protyle.ts index 292221c..b3b8905 100644 --- a/src/protyle.ts +++ b/src/protyle.ts @@ -50,6 +50,7 @@ export async function replaceSyncID(fileID: string, oldSyncID: string, newSyncID const search = encodeURI(IDsToAssetPath(fileID, oldSyncID)); // the API uses URI-encoded // find blocks containing that image const blocks = await findImageBlocks(search); + if(blocks.length === 0) return false; for(const block of blocks) { @@ -62,8 +63,11 @@ export async function replaceSyncID(fileID: string, oldSyncID: string, newSyncID for(const source of sources) { const newSource = IDsToAssetPath(fileID, newSyncID); - await replaceBlockContent(block.id, source, newSource); + const changed = await replaceBlockContent(block.id, source, newSource); + if(!changed) return false } + } + return true; } From ea9b0be8568e94f7203ef8883dc4e96644210598 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Sun, 6 Apr 2025 15:35:37 +0200 Subject: [PATCH 02/24] Update version --- package.json | 2 +- plugin.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f5dee54..3dd8f4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "siyuan-jsdraw-plugin", - "version": "0.2.0", + "version": "0.2.2", "type": "module", "description": "Include a whiteboard for freehand drawing anywhere in your documents.", "repository": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin", diff --git a/plugin.json b/plugin.json index 32dcc21..8c3ef88 100644 --- a/plugin.json +++ b/plugin.json @@ -2,7 +2,7 @@ "name": "siyuan-jsdraw-plugin", "author": "massivebox", "url": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin", - "version": "0.2.0", + "version": "0.2.2", "minAppVersion": "3.0.12", "backends": [ "windows", From e23cc424f840ce22417139e0ac4877fd9ce890f6 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Wed, 9 Apr 2025 22:53:40 +0200 Subject: [PATCH 03/24] Code quality improvements --- src/editor.ts | 172 +++++++++++++++++++++++++++++++++++++++++++++++ src/editorTab.ts | 126 ---------------------------------- src/index.ts | 11 ++- 3 files changed, 176 insertions(+), 133 deletions(-) create mode 100644 src/editor.ts delete mode 100644 src/editorTab.ts diff --git a/src/editor.ts b/src/editor.ts new file mode 100644 index 0000000..c744a2f --- /dev/null +++ b/src/editor.ts @@ -0,0 +1,172 @@ +import {MaterialIconProvider} from "@js-draw/material-icons"; +import {getFile, saveFile, uploadAsset} from "@/file"; +import {DATA_PATH, JSON_MIME, SVG_MIME, TOOLBAR_PATH} from "@/const"; +import {IDsToAssetPath} from "@/helper"; +import Editor, {BaseWidget, EditorEventType} from "js-draw"; +import {Dialog, Plugin, openTab, getFrontend} from "siyuan"; +import {replaceSyncID} from "@/protyle"; +import {removeFile} from "@/api"; + +export class PluginEditor { + + private readonly element: HTMLElement; + private readonly editor: Editor; + + private readonly fileID: string; + private syncID: string; + private readonly initialSyncID: string; + + getElement(): HTMLElement { return this.element; } + getEditor(): Editor { return this.editor; } + getFileID(): string { return this.fileID; } + getSyncID(): string { return this.syncID; } + getInitialSyncID(): string { return this.initialSyncID; } + + constructor(fileID: string, initialSyncID: string) { + + this.element = document.createElement("div"); + this.element.style.height = '100%'; + this.editor = new Editor(this.element, { + iconProvider: new MaterialIconProvider(), + }); + + this.fileID = fileID; + this.initialSyncID = initialSyncID; + this.syncID = initialSyncID; + + this.genToolbar() + + // restore drawing + getFile(DATA_PATH +IDsToAssetPath(fileID, initialSyncID)).then(svg => { + if(svg != null) { + this.editor.loadFromSVG(svg); + } + }); + + this.editor.dispatch(this.editor.setBackgroundStyle({ autoresize: true }), false); + this.editor.getRootElement().style.height = '100%'; + + } + + private async genToolbar() { + + const toolbar = this.editor.addToolbar(); + + // restore toolbar state + const toolbarState = await getFile(TOOLBAR_PATH); + if (toolbarState != null) { + toolbar.deserializeState(toolbarState); + } + + // save button + const saveButton = toolbar.addSaveButton(async () => { + await this.saveCallback(saveButton); + }); + + // save toolbar config on tool change (toolbar state is not saved in SVGs!) + this.editor.notifier.on(EditorEventType.ToolUpdated, () => { + saveFile(TOOLBAR_PATH, JSON_MIME, toolbar.serializeState()); + }); + + } + + private async saveCallback(saveButton: BaseWidget) { + + const svgElem = this.editor.toSVG(); + let newSyncID: string; + const oldSyncID = this.syncID; + + try { + newSyncID = (await uploadAsset(this.fileID, SVG_MIME, svgElem.outerHTML)).syncID; + if(newSyncID != oldSyncID) { + const changed = await replaceSyncID(this.fileID, oldSyncID, newSyncID); + if(!changed) { + alert( + "Error replacing old sync ID with new one! You may need to manually replace the file path." + + "\nTry saving the drawing again. This is a bug, please open an issue as soon as you can." + + "\nIf your document doesn't show the drawing, you can recover it from the SiYuan workspace directory." + ); + return; + } + await removeFile(DATA_PATH + IDsToAssetPath(this.fileID, oldSyncID)); + } + saveButton.setDisabled(true); + setTimeout(() => { // @todo improve save button feedback + saveButton.setDisabled(false); + }, 500); + } catch (error) { + alert("Error saving drawing! Enter developer mode to find the error, and a copy of the current status."); + console.error(error); + console.log("Couldn't save SVG: ", svgElem.outerHTML) + return; + } + + this.syncID = newSyncID; + + } + +} + +export class EditorManager { + + private editor: PluginEditor + + constructor(editor: PluginEditor) { + this.editor = editor; + } + + static registerTab(p: Plugin) { + p.addTab({ + 'type': "whiteboard", + init() { + const fileID = this.data.fileID; + const initialSyncID = this.data.initialSyncID; + if (fileID == null || initialSyncID == null) { + alert("File or Sync ID and path missing - couldn't open file.") + return; + } + const editor = new PluginEditor(fileID, initialSyncID); + this.element.appendChild(editor.getElement()); + } + }); + } + + toTab(p: Plugin) { + for(const tab of p.getOpenedTab()["whiteboard"]) { + if(tab.data.fileID == this.editor.getFileID()) { + alert("File is already open in another editor tab!"); + return; + } + } + openTab({ + app: p.app, + custom: { + title: 'Drawing', + icon: 'iconDraw', + id: "siyuan-jsdraw-pluginwhiteboard", + data: { + fileID: this.editor.getFileID(), + initialSyncID: this.editor.getInitialSyncID() + } + } + }); + } + + toDialog() { + const dialog = new Dialog({ + width: "100vw", + height: "100vh", + content: `
`, + }); + dialog.element.querySelector("#DrawingPanel").appendChild(this.editor.getElement()); + } + + open(p: Plugin) { + if(getFrontend() != "mobile") { + this.toTab(p); + } else { + this.toDialog(); + } + } + +} \ No newline at end of file diff --git a/src/editorTab.ts b/src/editorTab.ts deleted file mode 100644 index 001320d..0000000 --- a/src/editorTab.ts +++ /dev/null @@ -1,126 +0,0 @@ -import {Dialog, getFrontend, ITabModel, openTab, Plugin} from "siyuan" -import Editor, {BaseWidget, EditorEventType} from "js-draw"; -import { MaterialIconProvider } from '@js-draw/material-icons'; -import 'js-draw/styles'; -import {getFile, saveFile, uploadAsset} from "@/file"; -import {DATA_PATH, JSON_MIME, SVG_MIME, TOOLBAR_PATH} from "@/const"; -import {replaceSyncID} from "@/protyle"; -import {IDsToAssetPath} from "@/helper"; -import {removeFile} from "@/api"; - -export function openEditorTab(p: Plugin, fileID: string, initialSyncID: string) { - if(getFrontend() == "mobile") { - const dialog = new Dialog({ - width: "100vw", - height: "100vh", - content: `
`, - }); - createEditor(dialog.element.querySelector("#DrawingPanel"), fileID, initialSyncID); - return; - } - for(const tab of p.getOpenedTab()["whiteboard"]) { - if(tab.data.fileID == fileID) { - alert("File is already open in another editor tab!"); - return; - } - } - openTab({ - app: p.app, - custom: { - title: 'Drawing', - icon: 'iconDraw', - id: "siyuan-jsdraw-pluginwhiteboard", - data: { - fileID: fileID, - initialSyncID: initialSyncID - } - } - }); -} - -async function saveCallback(editor: Editor, fileID: string, oldSyncID: string, saveButton: BaseWidget): Promise { - - const svgElem = editor.toSVG(); - let newSyncID; - - try { - newSyncID = (await uploadAsset(fileID, SVG_MIME, svgElem.outerHTML)).syncID; - if(newSyncID != oldSyncID) { - const changed = await replaceSyncID(fileID, oldSyncID, newSyncID); - if(!changed) { - alert( - "Error replacing old sync ID with new one! You may need to manually replace the file path." + - "\nTry saving the drawing again. This is a bug, please open an issue as soon as you can." + - "\nIf your document doesn't show the drawing, you can recover it from the SiYuan workspace directory." - ); - return oldSyncID; - } - await removeFile(DATA_PATH + IDsToAssetPath(fileID, oldSyncID)); - } - saveButton.setDisabled(true); - setTimeout(() => { // @todo improve save button feedback - saveButton.setDisabled(false); - }, 500); - } catch (error) { - alert("Error saving drawing! Enter developer mode to find the error, and a copy of the current status."); - console.error(error); - console.log("Couldn't save SVG: ", svgElem.outerHTML) - return oldSyncID; - } - - return newSyncID - -} - -export function createEditor(element: HTMLElement, fileID: string, initialSyncID: string) { - - const editor = new Editor(element, { - iconProvider: new MaterialIconProvider(), - }); - - const toolbar = editor.addToolbar(); - - // restore toolbar state - getFile(TOOLBAR_PATH).then(toolbarState => { - if(toolbarState!= null) { - toolbar.deserializeState(toolbarState) - } - }); - // restore drawing - getFile(DATA_PATH +IDsToAssetPath(fileID, initialSyncID)).then(svg => { - if(svg != null) { - editor.loadFromSVG(svg); - } - }); - - let syncID = initialSyncID; - // save logic - const saveButton = toolbar.addSaveButton(() => { - saveCallback(editor, fileID, syncID, saveButton).then( - newSyncID => { - syncID = newSyncID - } - ) - }); - - // save toolbar config on tool change (toolbar state is not saved in SVGs!) - editor.notifier.on(EditorEventType.ToolUpdated, () => { - saveFile(TOOLBAR_PATH, JSON_MIME, toolbar.serializeState()); - }); - - editor.dispatch(editor.setBackgroundStyle({ autoresize: true }), false); - editor.getRootElement().style.height = '100%'; - -} - -export function editorTabInit(tab: ITabModel) { - - const fileID = tab.data.fileID; - const initialSyncID = tab.data.initialSyncID; - if (fileID == null || initialSyncID == null) { - alert("File or Sync ID and path missing - couldn't open file.") - return; - } - createEditor(tab.element, fileID, initialSyncID); - -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index adf4b94..16886a8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,18 +6,15 @@ import { findImgSrc, imgSrcToIDs, generateTimeString, generateRandomString } from "@/helper"; -import {editorTabInit, openEditorTab} from "@/editorTab"; import {migrate} from "@/migration"; +import {EditorManager, PluginEditor} from "@/editor"; export default class DrawJSPlugin extends Plugin { onload() { loadIcons(this); - this.addTab({ - 'type': "whiteboard", - init() { editorTabInit(this) } - }); + EditorManager.registerTab(this); migrate() this.protyleSlash = [{ @@ -28,7 +25,7 @@ export default class DrawJSPlugin extends Plugin { const fileID = generateRandomString(); const syncID = generateTimeString() + '-' + generateRandomString(); protyle.insert(getMarkdownBlock(fileID, syncID), true, false); - openEditorTab(this, fileID, syncID); + new EditorManager(new PluginEditor(fileID, syncID)).open(this) } }]; @@ -39,7 +36,7 @@ export default class DrawJSPlugin extends Plugin { icon: "iconDraw", label: "Edit with js-draw", click: () => { - openEditorTab(this, ids.fileID, ids.syncID); + new EditorManager(new PluginEditor(ids.fileID, ids.syncID)).open(this) } }) }) From 6bca12c9341ef59c54394f5564bb77ae6f3f8491 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Fri, 11 Apr 2025 18:42:45 +0200 Subject: [PATCH 04/24] File refactoring --- src/const.ts | 6 +- src/editor.ts | 45 ++++++++------- src/file.ts | 142 +++++++++++++++++++++++++++++++++-------------- src/helper.ts | 5 +- src/migration.ts | 16 ++++-- 5 files changed, 142 insertions(+), 72 deletions(-) diff --git a/src/const.ts b/src/const.ts index 2f8e88a..b7d60aa 100644 --- a/src/const.ts +++ b/src/const.ts @@ -2,8 +2,8 @@ export const SVG_MIME = "image/svg+xml"; export const JSON_MIME = "application/json"; export const DATA_PATH = "/data/"; export const ASSETS_PATH = "assets/"; -export const STORAGE_PATH = DATA_PATH + "storage/petal/siyuan-jsdraw-plugin"; -export const TOOLBAR_PATH = STORAGE_PATH + "/toolbar.json"; -export const CONFIG_PATH = STORAGE_PATH + "/conf.json"; +export const STORAGE_PATH = "/data/storage/petal/siyuan-jsdraw-plugin/"; +export const TOOLBAR_FILENAME = "toolbar.json"; +export const CONFIG_FILENAME = "conf.json"; export const EMBED_PATH = "/plugins/siyuan-jsdraw-plugin/webapp/?path="; export const DUMMY_HOST = "https://dummy.host/"; \ No newline at end of file diff --git a/src/editor.ts b/src/editor.ts index c744a2f..208bcc5 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -1,17 +1,18 @@ import {MaterialIconProvider} from "@js-draw/material-icons"; -import {getFile, saveFile, uploadAsset} from "@/file"; -import {DATA_PATH, JSON_MIME, SVG_MIME, TOOLBAR_PATH} from "@/const"; -import {IDsToAssetPath} from "@/helper"; +import {PluginAsset, PluginFile} from "@/file"; +import {JSON_MIME, STORAGE_PATH, SVG_MIME, TOOLBAR_FILENAME} from "@/const"; import Editor, {BaseWidget, EditorEventType} from "js-draw"; import {Dialog, Plugin, openTab, getFrontend} from "siyuan"; import {replaceSyncID} from "@/protyle"; -import {removeFile} from "@/api"; export class PluginEditor { private readonly element: HTMLElement; private readonly editor: Editor; + private drawingFile: PluginAsset; + private toolbarFile: PluginFile; + private readonly fileID: string; private syncID: string; private readonly initialSyncID: string; @@ -34,12 +35,13 @@ export class PluginEditor { this.initialSyncID = initialSyncID; this.syncID = initialSyncID; - this.genToolbar() + this.genToolbar(); // restore drawing - getFile(DATA_PATH +IDsToAssetPath(fileID, initialSyncID)).then(svg => { - if(svg != null) { - this.editor.loadFromSVG(svg); + this.drawingFile = new PluginAsset(fileID, initialSyncID, SVG_MIME); + this.drawingFile.loadFromSiYuanFS().then(() => { + if(this.drawingFile.getContent() != null) { + this.editor.loadFromSVG(this.drawingFile.getContent()); } }); @@ -52,11 +54,13 @@ export class PluginEditor { const toolbar = this.editor.addToolbar(); - // restore toolbar state - const toolbarState = await getFile(TOOLBAR_PATH); - if (toolbarState != null) { - toolbar.deserializeState(toolbarState); - } + // restore toolbarFile state + this.toolbarFile = new PluginFile(STORAGE_PATH, TOOLBAR_FILENAME, JSON_MIME); + this.toolbarFile.loadFromSiYuanFS().then(() => { + if(this.toolbarFile.getContent() != null) { + toolbar.deserializeState(this.toolbarFile.getContent()); + } + }); // save button const saveButton = toolbar.addSaveButton(async () => { @@ -65,7 +69,8 @@ export class PluginEditor { // save toolbar config on tool change (toolbar state is not saved in SVGs!) this.editor.notifier.on(EditorEventType.ToolUpdated, () => { - saveFile(TOOLBAR_PATH, JSON_MIME, toolbar.serializeState()); + this.toolbarFile.setContent(toolbar.serializeState()); + this.toolbarFile.save(); }); } @@ -77,18 +82,20 @@ export class PluginEditor { const oldSyncID = this.syncID; try { - newSyncID = (await uploadAsset(this.fileID, SVG_MIME, svgElem.outerHTML)).syncID; - if(newSyncID != oldSyncID) { - const changed = await replaceSyncID(this.fileID, oldSyncID, newSyncID); + this.drawingFile.setContent(svgElem.outerHTML); + await this.drawingFile.save(); + newSyncID = this.drawingFile.getSyncID(); + if(newSyncID != oldSyncID) { // supposed to replace protyle + const changed = await replaceSyncID(this.fileID, oldSyncID, newSyncID); // try to change protyle if(!changed) { alert( "Error replacing old sync ID with new one! You may need to manually replace the file path." + "\nTry saving the drawing again. This is a bug, please open an issue as soon as you can." + "\nIf your document doesn't show the drawing, you can recover it from the SiYuan workspace directory." ); - return; + return; // don't delete old drawing if protyle unchanged (could cause confusion) } - await removeFile(DATA_PATH + IDsToAssetPath(this.fileID, oldSyncID)); + await this.drawingFile.removeOld(oldSyncID); } saveButton.setDisabled(true); setTimeout(() => { // @todo improve save button feedback diff --git a/src/file.ts b/src/file.ts index 79e0e48..dc2a86c 100644 --- a/src/file.ts +++ b/src/file.ts @@ -1,52 +1,108 @@ -import {getFileBlob, putFile, upload} from "@/api"; -import {ASSETS_PATH} from "@/const"; -import {assetPathToIDs} from "@/helper"; +import {getFileBlob, putFile, removeFile, upload} from "@/api"; +import {ASSETS_PATH, DATA_PATH} from "@/const"; +import {assetPathToIDs, IDsToAssetName} from "@/helper"; -function toFile(title: string, content: string, mimeType: string){ - const blob = new Blob([content], { type: mimeType }); - return new File([blob], title, { type: mimeType }); -} +abstract class PluginFileBase { -// upload asset to the assets folder, return fileID and syncID -export async function uploadAsset(fileID: string, mimeType: string, content: string) { + protected content: string | null; - const file = toFile(fileID + ".svg", content, mimeType); + protected fileName: string; + protected folderPath: string; + protected mimeType: string; - let r = await upload('/' + ASSETS_PATH, [file]); - if(r.errFiles) { - throw new Error("Failed to upload file"); - } - return assetPathToIDs(r.succMap[file.name]); + getContent() { return this.content; } + setContent(content: string) { this.content = content; } + setFileName(fileName: string) { this.fileName = fileName; } -} - -export function saveFile(path: string, mimeType: string, content: string) { - - const file = toFile(path.split('/').pop(), content, mimeType); - - try { - putFile(path, false, file); - } catch (error) { - console.error("Error saving file:", error); - throw error; - } - -} - -export async function getFile(path: string) { - - const blob = await getFileBlob(path); - const jsonText = await blob.text(); - - // if we got a 404 api response, we will return null - try { - const res = JSON.parse(jsonText); - if(res.code == 404) { - return null; + private setFolderPath(folderPath: string) { + if(folderPath.startsWith('/') && folderPath.endsWith('/')) { + this.folderPath = folderPath; + }else{ + throw new Error("folderPath must start and end with /"); } - }catch {} + } - // js-draw expects a string! - return jsonText; + // folderPath must start and end with / + constructor(folderPath: string, fileName: string, mimeType: string) { + this.setFolderPath(folderPath); + this.fileName = fileName; + this.mimeType = mimeType; + } + + async loadFromSiYuanFS() { + const blob = await getFileBlob(this.folderPath + this.fileName); + const text = await blob.text(); + + try { + const res = JSON.parse(text); + if(res.code == 404) { + this.content = null; + return; + } + }catch {} + + this.content = text; + } + + async remove(customFilename?: string) { + let filename = customFilename || this.fileName; + await removeFile(this.folderPath + filename); + } + + protected toFile(customFilename?: string): File { + let filename = customFilename || this.fileName; + const blob = new Blob([this.content], { type: this.mimeType }); + return new File([blob], filename, { type: this.mimeType }); + } } + +export class PluginFile extends PluginFileBase { + + async save() { + const file = this.toFile(); + try { + await putFile(this.folderPath + this.fileName, false, file); + } catch (error) { + console.error("Error saving file:", error); + throw error; + } + } + +} + +export class PluginAsset extends PluginFileBase { + + private fileID: string + private syncID: string + + getFileID() { return this.fileID; } + getSyncID() { return this.syncID; } + + constructor(fileID: string, syncID: string, mimeType: string) { + super(DATA_PATH + ASSETS_PATH, IDsToAssetName(fileID, syncID), mimeType); + this.fileID = fileID; + this.syncID = syncID; + } + + async save() { + + const file = this.toFile(this.fileID + '.svg'); + + let r = await upload('/' + ASSETS_PATH, [file]); + if (r.errFiles) { + throw new Error("Failed to upload file"); + } + const ids = assetPathToIDs(r.succMap[file.name]) + + this.fileID = ids.fileID; + this.syncID = ids.syncID; + super.setFileName(IDsToAssetName(this.fileID, this.syncID)); + + } + + async removeOld(oldSyncID: string) { + await super.remove(IDsToAssetName(this.fileID, oldSyncID)); + } + +} \ No newline at end of file diff --git a/src/helper.ts b/src/helper.ts index ea4524d..22c3e8c 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -47,8 +47,11 @@ export function generateRandomString() { } +export function IDsToAssetName(fileID: string, syncID: string) { + return `${fileID}-${syncID}.svg`; +} export function IDsToAssetPath(fileID: string, syncID: string) { - return `${ASSETS_PATH}${fileID}-${syncID}.svg` + return `${ASSETS_PATH}${IDsToAssetName(fileID, syncID)}` } export function assetPathToIDs(assetPath: string): { fileID: string; syncID: string } | null { diff --git a/src/migration.ts b/src/migration.ts index 6ec3eac..2829aad 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -1,5 +1,5 @@ import {sql} from "@/api"; -import {getFile, uploadAsset} from "@/file"; +import {PluginAsset, PluginFile} from "@/file"; import {ASSETS_PATH, DATA_PATH, SVG_MIME} from "@/const"; import {replaceBlockContent} from "@/protyle"; import {generateRandomString, getMarkdownBlock} from "@/helper"; @@ -13,11 +13,15 @@ export async function migrate() { for(const block of blocks) { const oldFileID = extractID(block.markdown); if(oldFileID) { - const newFileID = generateRandomString() + "-" + oldFileID; - const file = await getFile(DATA_PATH + ASSETS_PATH + oldFileID + ".svg"); - const r = await uploadAsset(newFileID, SVG_MIME, file); - const newMarkdown = getMarkdownBlock(r.fileID, r.syncID); - await replaceBlockContent(block.id, block.markdown, newMarkdown); + const oldFile = new PluginFile(DATA_PATH + ASSETS_PATH, oldFileID + '.svg', SVG_MIME); + await oldFile.loadFromSiYuanFS(); + const newFile = new PluginAsset(generateRandomString(), oldFileID, SVG_MIME); + newFile.setContent(oldFile.getContent()); + await newFile.save(); + const newMarkdown = getMarkdownBlock(newFile.getFileID(), newFile.getSyncID()); + if(await replaceBlockContent(block.id, block.markdown, newMarkdown)) { + await oldFile.remove(); + } } } From fc4ce8e69e39cef30283564040c1e5a178dc335e Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Tue, 15 Apr 2025 19:42:43 +0200 Subject: [PATCH 05/24] Add config menu framework Options don't do anything as of now, but they are saved and loaded --- src/config.ts | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/helper.ts | 7 ++++ src/index.ts | 12 +++++- 3 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 src/config.ts diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..8b293db --- /dev/null +++ b/src/config.ts @@ -0,0 +1,110 @@ +import {PluginFile} from "@/file"; +import {CONFIG_FILENAME, JSON_MIME, STORAGE_PATH} from "@/const"; +import {Plugin} from "siyuan"; +import {SettingUtils} from "@/libs/setting-utils"; +import {validateColor} from "@/helper"; + +type Options = { + autoResize: boolean, + background: string + analytics: boolean +}; + +export class PluginConfig { + + private file: PluginFile; + + options: Options; + + constructor() { + this.file = new PluginFile(STORAGE_PATH, CONFIG_FILENAME, JSON_MIME); + } + + async load() { + await this.file.loadFromSiYuanFS(); + this.options = JSON.parse(this.file.getContent()); + if(this.options == null) { + this.loadDefaultConfig(); + } + } + + private loadDefaultConfig() { + this.options = { + autoResize: true, + background: "#000000", + analytics: true + }; + } + + async save() { + this.file.setContent(JSON.stringify(this.options)); + await this.file.save(); + } + + setConfig(config: Options) { + if(!validateColor(config.background)) { + alert("Invalid background color! Please enter an HEX color, like #000000 (black) or #FFFFFF (white)"); + config.background = this.options.background; + } + + this.options = config; + } + +} + +export class PluginConfigViewer { + + config: PluginConfig; + settingUtils: SettingUtils; + plugin: Plugin; + + constructor(config: PluginConfig, plugin: Plugin) { + this.config = config; + this.plugin = plugin; + this.populateSettingMenu(); + } + + populateSettingMenu() { + + this.settingUtils = new SettingUtils({ + plugin: this.plugin, + callback: async (data) => { + this.config.setConfig(data); + await this.config.save(); + } + }); + + this.settingUtils.addItem({ + key: "autoResize", + title: "Auto Resize", + description: "Enable to automatically resize the drawing area according to your strokes on new drawings", + value: this.config.options.autoResize, + type: 'checkbox' + }); + + this.settingUtils.addItem({ + key: "background", + title: "Default Background Color", + description: "Default background color of the drawing area for new drawings in hexadecimal.", + value: this.config.options.background, + type: 'textarea', + }); + + this.settingUtils.addItem({ + key: "analytics", + title: "Analytics", + description: ` + Enable to send anonymous usage data to the developer. + Privacy + `, + value: this.config.options.analytics, + type: 'checkbox' + }); + + } + + load() { + return this.settingUtils.load(); + } + +} \ No newline at end of file diff --git a/src/helper.ts b/src/helper.ts index 22c3e8c..5a08636 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -105,4 +105,11 @@ export function imgSrcToIDs(imgSrc: string | null): { fileID: string; syncID: st return assetPathToIDs(imgSrc); +} + +export function validateColor(hex: string) { + hex = hex.replace('#', ''); + return typeof hex === 'string' + && hex.length === 6 + && !isNaN(Number('0x' + hex)) } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 16886a8..78735ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,15 +8,23 @@ import { } from "@/helper"; import {migrate} from "@/migration"; import {EditorManager, PluginEditor} from "@/editor"; +import {PluginConfig, PluginConfigViewer} from "@/config"; export default class DrawJSPlugin extends Plugin { - onload() { + config: PluginConfig; + + async onload() { loadIcons(this); EditorManager.registerTab(this); migrate() + this.config = new PluginConfig(); + await this.config.load(); + let configViewer = new PluginConfigViewer(this.config, this); + await configViewer.load(); + this.protyleSlash = [{ id: "insert-drawing", filter: ["Insert Drawing", "Add drawing", "whiteboard", "freehand", "graphics", "jsdraw"], @@ -31,7 +39,7 @@ export default class DrawJSPlugin extends Plugin { this.eventBus.on("open-menu-image", (e: any) => { const ids = imgSrcToIDs(findImgSrc(e.detail.element)); - if(ids === null) return; + if (ids === null) return; e.detail.menu.addItem({ icon: "iconDraw", label: "Edit with js-draw", From fe3250587390e98a8463c0ecd7c014c3bbcf2093 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Wed, 16 Apr 2025 23:56:24 +0200 Subject: [PATCH 06/24] Implement analytics --- src/analytics.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++++ src/config.ts | 17 +++++++++++++---- src/index.ts | 36 ++++++++++++++++++++++++++++++++---- 3 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 src/analytics.ts diff --git a/src/analytics.ts b/src/analytics.ts new file mode 100644 index 0000000..13d37be --- /dev/null +++ b/src/analytics.ts @@ -0,0 +1,46 @@ +import {getBackend, getFrontend} from "siyuan"; +import {JSON_MIME} from "@/const"; +import packageJson from '../package.json' assert { type: 'json' }; + +export class Analytics { + + private readonly enabled: boolean; + + private static readonly ENDPOINT = 'https://stats.massive.box/api/send_noua'; + private static readonly WEBSITE_ID = '0a1ebbc1-d702-4f64-86ed-f62dcde9b522'; + + constructor(enabled: boolean) { + this.enabled = enabled; + } + + async sendEvent(name: string) { + + if(!this.enabled) return; + + const sendData = (name == 'load' || name == 'install') ? + { + 'appVersion': window.navigator.userAgent.split(' ')[0], + 'pluginVersion': packageJson.version, + 'frontend': getFrontend(), + 'backend': getBackend(), + 'language': navigator.language, + } : {}; + + await fetch(Analytics.ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': JSON_MIME, + }, + body: JSON.stringify({ + type: 'event', + payload: { + website: Analytics.WEBSITE_ID, + name: name, + data: sendData, + }, + }) + }) + + } + +} \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 8b293db..b744fb4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,7 +5,7 @@ import {SettingUtils} from "@/libs/setting-utils"; import {validateColor} from "@/helper"; type Options = { - autoResize: boolean, + autoResize: boolean background: string analytics: boolean }; @@ -15,12 +15,16 @@ export class PluginConfig { private file: PluginFile; options: Options; + private firstRun: boolean; + + getFirstRun() { return this.firstRun } constructor() { this.file = new PluginFile(STORAGE_PATH, CONFIG_FILENAME, JSON_MIME); } async load() { + this.firstRun = false; await this.file.loadFromSiYuanFS(); this.options = JSON.parse(this.file.getContent()); if(this.options == null) { @@ -32,8 +36,9 @@ export class PluginConfig { this.options = { autoResize: true, background: "#000000", - analytics: true + analytics: true, }; + this.firstRun = true; } async save() { @@ -69,7 +74,11 @@ export class PluginConfigViewer { this.settingUtils = new SettingUtils({ plugin: this.plugin, callback: async (data) => { - this.config.setConfig(data); + this.config.setConfig({ + analytics: data.analytics, + autoResize: data.autoResize, + background: data.background, + }); await this.config.save(); } }); @@ -95,7 +104,7 @@ export class PluginConfigViewer { title: "Analytics", description: ` Enable to send anonymous usage data to the developer. - Privacy + Privacy `, value: this.config.options.analytics, type: 'checkbox' diff --git a/src/index.ts b/src/index.ts index 78735ce..2bd05c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,10 +9,12 @@ import { import {migrate} from "@/migration"; import {EditorManager, PluginEditor} from "@/editor"; import {PluginConfig, PluginConfigViewer} from "@/config"; +import {Analytics} from "@/analytics"; export default class DrawJSPlugin extends Plugin { config: PluginConfig; + analytics: Analytics; async onload() { @@ -20,16 +22,15 @@ export default class DrawJSPlugin extends Plugin { EditorManager.registerTab(this); migrate() - this.config = new PluginConfig(); - await this.config.load(); - let configViewer = new PluginConfigViewer(this.config, this); - await configViewer.load(); + await this.startConfig(); + await this.startAnalytics(); this.protyleSlash = [{ id: "insert-drawing", filter: ["Insert Drawing", "Add drawing", "whiteboard", "freehand", "graphics", "jsdraw"], html: getMenuHTML("iconDraw", this.i18n.insertDrawing), callback: (protyle: Protyle) => { + void this.analytics.sendEvent('create'); const fileID = generateRandomString(); const syncID = generateTimeString() + '-' + generateRandomString(); protyle.insert(getMarkdownBlock(fileID, syncID), true, false); @@ -44,6 +45,7 @@ export default class DrawJSPlugin extends Plugin { icon: "iconDraw", label: "Edit with js-draw", click: () => { + void this.analytics.sendEvent('edit'); new EditorManager(new PluginEditor(ids.fileID, ids.syncID)).open(this) } }) @@ -51,4 +53,30 @@ export default class DrawJSPlugin extends Plugin { } + onunload() { + void this.analytics.sendEvent("unload"); + } + + uninstall() { + void this.analytics.sendEvent("uninstall"); + } + + private async startConfig() { + this.config = new PluginConfig(); + await this.config.load(); + let configViewer = new PluginConfigViewer(this.config, this); + await configViewer.load(); + } + + private async startAnalytics() { + this.analytics = new Analytics(this.config.options.analytics); + if(this.config.getFirstRun()) { + await this.config.save(); + void this.analytics.sendEvent('install'); + }else{ + void this.analytics.sendEvent('load'); + } + } + + } \ No newline at end of file From 7e4da82b821b3d02f4a3c0d4974c5adb83b1747e Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Thu, 17 Apr 2025 15:16:07 +0200 Subject: [PATCH 07/24] Get initial Sync ID from protyle Related to issue #9 --- src/editor.ts | 50 ++++++++++++++++++++------------------------------ src/index.ts | 4 ++-- src/protyle.ts | 47 +++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 63 insertions(+), 38 deletions(-) diff --git a/src/editor.ts b/src/editor.ts index 208bcc5..17f9c86 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -3,7 +3,7 @@ import {PluginAsset, PluginFile} from "@/file"; import {JSON_MIME, STORAGE_PATH, SVG_MIME, TOOLBAR_FILENAME} from "@/const"; import Editor, {BaseWidget, EditorEventType} from "js-draw"; import {Dialog, Plugin, openTab, getFrontend} from "siyuan"; -import {replaceSyncID} from "@/protyle"; +import {findSyncIDInProtyle, replaceSyncID} from "@/protyle"; export class PluginEditor { @@ -15,15 +15,15 @@ export class PluginEditor { private readonly fileID: string; private syncID: string; - private readonly initialSyncID: string; getElement(): HTMLElement { return this.element; } getEditor(): Editor { return this.editor; } getFileID(): string { return this.fileID; } getSyncID(): string { return this.syncID; } - getInitialSyncID(): string { return this.initialSyncID; } - constructor(fileID: string, initialSyncID: string) { + constructor(fileID: string) { + + this.fileID = fileID; this.element = document.createElement("div"); this.element.style.height = '100%'; @@ -31,22 +31,20 @@ export class PluginEditor { iconProvider: new MaterialIconProvider(), }); - this.fileID = fileID; - this.initialSyncID = initialSyncID; - this.syncID = initialSyncID; - - this.genToolbar(); - - // restore drawing - this.drawingFile = new PluginAsset(fileID, initialSyncID, SVG_MIME); - this.drawingFile.loadFromSiYuanFS().then(() => { - if(this.drawingFile.getContent() != null) { - this.editor.loadFromSVG(this.drawingFile.getContent()); - } + this.genToolbar().then(() => { + this.editor.dispatch(this.editor.setBackgroundStyle({ autoresize: true }), false); + this.editor.getRootElement().style.height = '100%'; }); - this.editor.dispatch(this.editor.setBackgroundStyle({ autoresize: true }), false); - this.editor.getRootElement().style.height = '100%'; + findSyncIDInProtyle(this.fileID).then(async (syncID) => { + this.syncID = syncID; + // restore drawing + this.drawingFile = new PluginAsset(this.fileID, syncID, SVG_MIME); + await this.drawingFile.loadFromSiYuanFS(); + if(this.drawingFile.getContent() != null) { + await this.editor.loadFromSVG(this.drawingFile.getContent()); + } + }); } @@ -127,24 +125,17 @@ export class EditorManager { 'type': "whiteboard", init() { const fileID = this.data.fileID; - const initialSyncID = this.data.initialSyncID; - if (fileID == null || initialSyncID == null) { - alert("File or Sync ID and path missing - couldn't open file.") + if (fileID == null) { + alert("File ID missing - couldn't open file.") return; } - const editor = new PluginEditor(fileID, initialSyncID); + const editor = new PluginEditor(fileID); this.element.appendChild(editor.getElement()); } }); } toTab(p: Plugin) { - for(const tab of p.getOpenedTab()["whiteboard"]) { - if(tab.data.fileID == this.editor.getFileID()) { - alert("File is already open in another editor tab!"); - return; - } - } openTab({ app: p.app, custom: { @@ -153,7 +144,6 @@ export class EditorManager { id: "siyuan-jsdraw-pluginwhiteboard", data: { fileID: this.editor.getFileID(), - initialSyncID: this.editor.getInitialSyncID() } } }); @@ -168,7 +158,7 @@ export class EditorManager { dialog.element.querySelector("#DrawingPanel").appendChild(this.editor.getElement()); } - open(p: Plugin) { + async open(p: Plugin) { if(getFrontend() != "mobile") { this.toTab(p); } else { diff --git a/src/index.ts b/src/index.ts index 2bd05c8..1e0777a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,7 +34,7 @@ export default class DrawJSPlugin extends Plugin { const fileID = generateRandomString(); const syncID = generateTimeString() + '-' + generateRandomString(); protyle.insert(getMarkdownBlock(fileID, syncID), true, false); - new EditorManager(new PluginEditor(fileID, syncID)).open(this) + new EditorManager(new PluginEditor(fileID)).open(this); } }]; @@ -46,7 +46,7 @@ export default class DrawJSPlugin extends Plugin { label: "Edit with js-draw", click: () => { void this.analytics.sendEvent('edit'); - new EditorManager(new PluginEditor(ids.fileID, ids.syncID)).open(this) + new EditorManager(new PluginEditor(ids.fileID)).open(this) } }) }) diff --git a/src/protyle.ts b/src/protyle.ts index b3b8905..2914b3c 100644 --- a/src/protyle.ts +++ b/src/protyle.ts @@ -1,5 +1,37 @@ import {getBlockByID, sql, updateBlock} from "@/api"; -import {IDsToAssetPath} from "@/helper"; +import {assetPathToIDs, IDsToAssetPath} from "@/helper"; + +export async function findSyncIDInProtyle(fileID: string, iter?: number): Promise { + + const search = `assets/${fileID}-`; + const blocks = await findImageBlocks(search); + + let syncID = null; + + for(const block of blocks) { + const sources = extractImageSourcesFromMarkdown(block.markdown, search); + for(const source of sources) { + const ids = assetPathToIDs(source); + if(syncID == null) { + syncID = ids.syncID; + }else if(ids.syncID !== syncID) { + throw new Error("Multiple syncIDs found"); + } + } + } + + if(!iter) iter = 0; + if(syncID == null) { + // when the block has just been created, we need to wait a bit before it can be found + if(iter < 4) { // cap max time at 2s, it should be ok by then + await new Promise(resolve => setTimeout(resolve, 500)); + return await findSyncIDInProtyle(fileID, iter + 1); + } + } + + return syncID; + +} export async function findImageBlocks(src: string) { @@ -45,6 +77,13 @@ export async function replaceBlockContent( } } +function extractImageSourcesFromMarkdown(markdown: string, mustStartWith?: string) { + const imageRegex = /!\[.*?\]\((.*?)\)/g; // only get images + return Array.from(markdown.matchAll(imageRegex)) + .map(match => match[1]) + .filter(source => source.startsWith(mustStartWith)) // discard other images +} + export async function replaceSyncID(fileID: string, oldSyncID: string, newSyncID: string) { const search = encodeURI(IDsToAssetPath(fileID, oldSyncID)); // the API uses URI-encoded @@ -56,12 +95,8 @@ export async function replaceSyncID(fileID: string, oldSyncID: string, newSyncID // get all the image sources, with parameters const markdown = block.markdown; - const imageRegex = /!\[.*?\]\((.*?)\)/g; // only get images - const sources = Array.from(markdown.matchAll(imageRegex)) - .map(match => match[1]) - .filter(source => source.startsWith(search)) // discard other images - for(const source of sources) { + for(const source of extractImageSourcesFromMarkdown(markdown, search)) { const newSource = IDsToAssetPath(fileID, newSyncID); const changed = await replaceBlockContent(block.id, source, newSource); if(!changed) return false From e815442881263b234f3163d0305202d34a622028 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Thu, 17 Apr 2025 16:08:26 +0200 Subject: [PATCH 08/24] Editor default options --- src/config.ts | 44 +++++++++++++++++++++++++++++++++++--------- src/editor.ts | 23 ++++++++++++++++------- src/index.ts | 4 ++-- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/src/config.ts b/src/config.ts index b744fb4..8ad50dd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,11 +5,17 @@ import {SettingUtils} from "@/libs/setting-utils"; import {validateColor} from "@/helper"; type Options = { - autoResize: boolean + grid: boolean background: string + dialogOnDesktop: boolean analytics: boolean }; +export type DefaultEditorOptions = { + grid: boolean + background: string +} + export class PluginConfig { private file: PluginFile; @@ -23,6 +29,13 @@ export class PluginConfig { this.file = new PluginFile(STORAGE_PATH, CONFIG_FILENAME, JSON_MIME); } + getDefaultEditorOptions(): DefaultEditorOptions { + return { + grid: this.options.grid, + background: this.options.background, + }; + } + async load() { this.firstRun = false; await this.file.loadFromSiYuanFS(); @@ -34,8 +47,9 @@ export class PluginConfig { private loadDefaultConfig() { this.options = { - autoResize: true, + grid: true, background: "#000000", + dialogOnDesktop: false, analytics: true, }; this.firstRun = true; @@ -75,30 +89,42 @@ export class PluginConfigViewer { plugin: this.plugin, callback: async (data) => { this.config.setConfig({ - analytics: data.analytics, - autoResize: data.autoResize, + grid: data.grid, background: data.background, + dialogOnDesktop: data.dialogOnDesktop, + analytics: data.analytics, }); await this.config.save(); } }); this.settingUtils.addItem({ - key: "autoResize", - title: "Auto Resize", - description: "Enable to automatically resize the drawing area according to your strokes on new drawings", - value: this.config.options.autoResize, + key: "grid", + title: "Enable grid by default", + description: "Enable to automatically turn on the grid on new drawings.", + value: this.config.options.grid, type: 'checkbox' }); this.settingUtils.addItem({ key: "background", - title: "Default Background Color", + title: "Default background Color", description: "Default background color of the drawing area for new drawings in hexadecimal.", value: this.config.options.background, type: 'textarea', }); + this.settingUtils.addItem({ + key: "dialogOnDesktop", + title: "Open editor as dialog on desktop", + description: ` + Dialog mode provides a larger drawing area, but it's not as handy to use as tabs (default).
+ The editor will always open as a dialog on mobile. + `, + value: this.config.options.grid, + type: 'checkbox' + }); + this.settingUtils.addItem({ key: "analytics", title: "Analytics", diff --git a/src/editor.ts b/src/editor.ts index 17f9c86..9c5e524 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -1,9 +1,11 @@ import {MaterialIconProvider} from "@js-draw/material-icons"; import {PluginAsset, PluginFile} from "@/file"; import {JSON_MIME, STORAGE_PATH, SVG_MIME, TOOLBAR_FILENAME} from "@/const"; -import Editor, {BaseWidget, EditorEventType} from "js-draw"; -import {Dialog, Plugin, openTab, getFrontend} from "siyuan"; +import Editor, {BackgroundComponentBackgroundType, BaseWidget, Color4, EditorEventType} from "js-draw"; +import {Dialog, getFrontend, openTab, Plugin} from "siyuan"; import {findSyncIDInProtyle, replaceSyncID} from "@/protyle"; +import DrawJSPlugin from "@/index"; +import {DefaultEditorOptions} from "@/config"; export class PluginEditor { @@ -21,7 +23,7 @@ export class PluginEditor { getFileID(): string { return this.fileID; } getSyncID(): string { return this.syncID; } - constructor(fileID: string) { + constructor(fileID: string, defaultEditorOptions: DefaultEditorOptions) { this.fileID = fileID; @@ -43,6 +45,13 @@ export class PluginEditor { await this.drawingFile.loadFromSiYuanFS(); if(this.drawingFile.getContent() != null) { await this.editor.loadFromSVG(this.drawingFile.getContent()); + }else{ + // it's a new drawing + this.editor.dispatch(this.editor.setBackgroundStyle({ + color: Color4.fromHex(defaultEditorOptions.background), + type: defaultEditorOptions.grid ? BackgroundComponentBackgroundType.Grid : BackgroundComponentBackgroundType.SolidColor, + autoresize: true + })); } }); @@ -120,7 +129,7 @@ export class EditorManager { this.editor = editor; } - static registerTab(p: Plugin) { + static registerTab(p: DrawJSPlugin) { p.addTab({ 'type': "whiteboard", init() { @@ -129,7 +138,7 @@ export class EditorManager { alert("File ID missing - couldn't open file.") return; } - const editor = new PluginEditor(fileID); + const editor = new PluginEditor(fileID, p.config.getDefaultEditorOptions()); this.element.appendChild(editor.getElement()); } }); @@ -158,8 +167,8 @@ export class EditorManager { dialog.element.querySelector("#DrawingPanel").appendChild(this.editor.getElement()); } - async open(p: Plugin) { - if(getFrontend() != "mobile") { + async open(p: DrawJSPlugin) { + if(getFrontend() != "mobile" && !p.config.options.dialogOnDesktop) { this.toTab(p); } else { this.toDialog(); diff --git a/src/index.ts b/src/index.ts index 1e0777a..4c5a0fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,7 +34,7 @@ export default class DrawJSPlugin extends Plugin { const fileID = generateRandomString(); const syncID = generateTimeString() + '-' + generateRandomString(); protyle.insert(getMarkdownBlock(fileID, syncID), true, false); - new EditorManager(new PluginEditor(fileID)).open(this); + new EditorManager(new PluginEditor(fileID, this.config.getDefaultEditorOptions())).open(this); } }]; @@ -46,7 +46,7 @@ export default class DrawJSPlugin extends Plugin { label: "Edit with js-draw", click: () => { void this.analytics.sendEvent('edit'); - new EditorManager(new PluginEditor(ids.fileID)).open(this) + new EditorManager(new PluginEditor(ids.fileID, this.config.getDefaultEditorOptions())).open(this) } }) }) From 3a05d36f8c0d77e97b53aec77b0bdfeaf8933b1c Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Thu, 17 Apr 2025 22:27:38 +0200 Subject: [PATCH 09/24] Fixes + Version bump --- package.json | 2 +- plugin.json | 2 +- src/config.ts | 2 +- src/editor.ts | 33 +++++++++++++++++++++------------ src/index.ts | 6 +++--- src/protyle.ts | 6 +++++- 6 files changed, 32 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 3dd8f4f..f355588 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "siyuan-jsdraw-plugin", - "version": "0.2.2", + "version": "0.3.0", "type": "module", "description": "Include a whiteboard for freehand drawing anywhere in your documents.", "repository": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin", diff --git a/plugin.json b/plugin.json index 8c3ef88..3f18451 100644 --- a/plugin.json +++ b/plugin.json @@ -2,7 +2,7 @@ "name": "siyuan-jsdraw-plugin", "author": "massivebox", "url": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin", - "version": "0.2.2", + "version": "0.3.0", "minAppVersion": "3.0.12", "backends": [ "windows", diff --git a/src/config.ts b/src/config.ts index 8ad50dd..52fd764 100644 --- a/src/config.ts +++ b/src/config.ts @@ -121,7 +121,7 @@ export class PluginConfigViewer { Dialog mode provides a larger drawing area, but it's not as handy to use as tabs (default).
The editor will always open as a dialog on mobile. `, - value: this.config.options.grid, + value: this.config.options.dialogOnDesktop, type: 'checkbox' }); diff --git a/src/editor.ts b/src/editor.ts index 9c5e524..8680a40 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -6,6 +6,7 @@ import {Dialog, getFrontend, openTab, Plugin} from "siyuan"; import {findSyncIDInProtyle, replaceSyncID} from "@/protyle"; import DrawJSPlugin from "@/index"; import {DefaultEditorOptions} from "@/config"; +import 'js-draw/styles'; export class PluginEditor { @@ -39,10 +40,21 @@ export class PluginEditor { }); findSyncIDInProtyle(this.fileID).then(async (syncID) => { + + if(syncID == null) { + alert( + "Couldn't find SyncID in protyle for this file.\n" + + "Make sure the drawing you're trying to edit exists in a note.\n" + + "Close this editor tab now, and try to open the editor again." + ); + return; + } + this.syncID = syncID; // restore drawing this.drawingFile = new PluginAsset(this.fileID, syncID, SVG_MIME); await this.drawingFile.loadFromSiYuanFS(); + if(this.drawingFile.getContent() != null) { await this.editor.loadFromSVG(this.drawingFile.getContent()); }else{ @@ -53,6 +65,9 @@ export class PluginEditor { autoresize: true })); } + + }).catch((error) => { + alert("Error loading drawing: " + error); }); } @@ -94,14 +109,7 @@ export class PluginEditor { newSyncID = this.drawingFile.getSyncID(); if(newSyncID != oldSyncID) { // supposed to replace protyle const changed = await replaceSyncID(this.fileID, oldSyncID, newSyncID); // try to change protyle - if(!changed) { - alert( - "Error replacing old sync ID with new one! You may need to manually replace the file path." + - "\nTry saving the drawing again. This is a bug, please open an issue as soon as you can." + - "\nIf your document doesn't show the drawing, you can recover it from the SiYuan workspace directory." - ); - return; // don't delete old drawing if protyle unchanged (could cause confusion) - } + if(!changed) throw new Error("Couldn't replace old images in protyle"); await this.drawingFile.removeOld(oldSyncID); } saveButton.setDisabled(true); @@ -109,7 +117,8 @@ export class PluginEditor { saveButton.setDisabled(false); }, 500); } catch (error) { - alert("Error saving drawing! Enter developer mode to find the error, and a copy of the current status."); + alert("Error saving! The current drawing has been copied to your clipboard. You may need to create a new drawing and paste it there."); + await navigator.clipboard.writeText(svgElem.outerHTML); console.error(error); console.log("Couldn't save SVG: ", svgElem.outerHTML) return; @@ -125,8 +134,8 @@ export class EditorManager { private editor: PluginEditor - constructor(editor: PluginEditor) { - this.editor = editor; + constructor(fileID: string, defaultEditorOptions: DefaultEditorOptions) { + this.editor = new PluginEditor(fileID, defaultEditorOptions); } static registerTab(p: DrawJSPlugin) { @@ -161,7 +170,7 @@ export class EditorManager { toDialog() { const dialog = new Dialog({ width: "100vw", - height: "100vh", + height: getFrontend() == "mobile" ? "100vh" : "90vh", content: `
`, }); dialog.element.querySelector("#DrawingPanel").appendChild(this.editor.getElement()); diff --git a/src/index.ts b/src/index.ts index 4c5a0fe..599bf3a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import { imgSrcToIDs, generateTimeString, generateRandomString } from "@/helper"; import {migrate} from "@/migration"; -import {EditorManager, PluginEditor} from "@/editor"; +import {EditorManager} from "@/editor"; import {PluginConfig, PluginConfigViewer} from "@/config"; import {Analytics} from "@/analytics"; @@ -34,7 +34,7 @@ export default class DrawJSPlugin extends Plugin { const fileID = generateRandomString(); const syncID = generateTimeString() + '-' + generateRandomString(); protyle.insert(getMarkdownBlock(fileID, syncID), true, false); - new EditorManager(new PluginEditor(fileID, this.config.getDefaultEditorOptions())).open(this); + new EditorManager(fileID, this.config.getDefaultEditorOptions()).open(this); } }]; @@ -46,7 +46,7 @@ export default class DrawJSPlugin extends Plugin { label: "Edit with js-draw", click: () => { void this.analytics.sendEvent('edit'); - new EditorManager(new PluginEditor(ids.fileID, this.config.getDefaultEditorOptions())).open(this) + new EditorManager(ids.fileID, this.config.getDefaultEditorOptions()).open(this); } }) }) diff --git a/src/protyle.ts b/src/protyle.ts index 2914b3c..5c3e842 100644 --- a/src/protyle.ts +++ b/src/protyle.ts @@ -15,7 +15,11 @@ export async function findSyncIDInProtyle(fileID: string, iter?: number): Promis if(syncID == null) { syncID = ids.syncID; }else if(ids.syncID !== syncID) { - throw new Error("Multiple syncIDs found"); + throw new Error( + "Multiple syncIDs found in documents. Remove the drawings that don't exist from your documents.\n" + + "Sync conflict copies can cause this error, so make sure to delete them, or at least the js-draw drawings they contain.\n" + + "File IDs must be unique. Close this editor tab now." + ); } } } From f35342a791d92f80d34cbc4bcddee6fa85a8f694 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Sun, 20 Apr 2025 22:16:48 +0200 Subject: [PATCH 10/24] Start workin on i18n --- public/i18n/en_US.json | 24 +++++++++++++++++++++++- src/config.ts | 23 +++++++++-------------- src/editor.ts | 4 ++-- src/index.ts | 2 +- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json index b6d2382..88c96a3 100644 --- a/public/i18n/en_US.json +++ b/public/i18n/en_US.json @@ -1,3 +1,25 @@ { - "insertDrawing": "Insert Drawing" + "insertDrawing": "Insert Drawing", + "editDrawing": "Edit with js-draw", + "errNoFileID": "File ID missing - couldn't open file.", + "drawing": "Drawing", + "settings": { + "name": "js-draw Plugin Settings", + "grid": { + "title": "Enable grid by default", + "description": "Enable to automatically turn on the grid on new drawings." + }, + "background": { + "title": "Default background Color", + "description": "Default background color for new drawings, in hexadecimal." + }, + "dialogOnDesktop": { + "title": "Open editor as dialog on desktop", + "description": "Dialog mode provides a larger drawing area, but it's not as handy to use as tabs (default).
The editor will always open as a dialog on mobile." + }, + "analytics": { + "title": "Analytics", + "description": "Enable to send anonymous usage data to the developer. Privacy Policy" + } + } } \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 52fd764..4491329 100644 --- a/src/config.ts +++ b/src/config.ts @@ -87,6 +87,7 @@ export class PluginConfigViewer { this.settingUtils = new SettingUtils({ plugin: this.plugin, + name: this.plugin.i18n.settings.name, callback: async (data) => { this.config.setConfig({ grid: data.grid, @@ -100,38 +101,32 @@ export class PluginConfigViewer { this.settingUtils.addItem({ key: "grid", - title: "Enable grid by default", - description: "Enable to automatically turn on the grid on new drawings.", + title: this.plugin.i18n.settings.grid.title, + description: this.plugin.i18n.settings.grid.description, value: this.config.options.grid, type: 'checkbox' }); this.settingUtils.addItem({ key: "background", - title: "Default background Color", - description: "Default background color of the drawing area for new drawings in hexadecimal.", + title: this.plugin.i18n.settings.background.title, + description: this.plugin.i18n.settings.background.description, value: this.config.options.background, type: 'textarea', }); this.settingUtils.addItem({ key: "dialogOnDesktop", - title: "Open editor as dialog on desktop", - description: ` - Dialog mode provides a larger drawing area, but it's not as handy to use as tabs (default).
- The editor will always open as a dialog on mobile. - `, + title: this.plugin.i18n.settings.dialogOnDesktop.title, + description: this.plugin.i18n.settings.dialogOnDesktop.description, value: this.config.options.dialogOnDesktop, type: 'checkbox' }); this.settingUtils.addItem({ key: "analytics", - title: "Analytics", - description: ` - Enable to send anonymous usage data to the developer. - Privacy - `, + title: this.plugin.i18n.settings.analytics.title, + description: this.plugin.i18n.settings.analytics.description, value: this.config.options.analytics, type: 'checkbox' }); diff --git a/src/editor.ts b/src/editor.ts index 8680a40..d8bb16e 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -144,7 +144,7 @@ export class EditorManager { init() { const fileID = this.data.fileID; if (fileID == null) { - alert("File ID missing - couldn't open file.") + alert(p.i18n.errNoFileID); return; } const editor = new PluginEditor(fileID, p.config.getDefaultEditorOptions()); @@ -157,7 +157,7 @@ export class EditorManager { openTab({ app: p.app, custom: { - title: 'Drawing', + title: p.i18n.drawing, icon: 'iconDraw', id: "siyuan-jsdraw-pluginwhiteboard", data: { diff --git a/src/index.ts b/src/index.ts index 599bf3a..1ef36aa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,7 +43,7 @@ export default class DrawJSPlugin extends Plugin { if (ids === null) return; e.detail.menu.addItem({ icon: "iconDraw", - label: "Edit with js-draw", + label: this.i18n.editDrawing, click: () => { void this.analytics.sendEvent('edit'); new EditorManager(ids.fileID, this.config.getDefaultEditorOptions()).open(this); From 8d4779b8fe3828e1e97e79a2e2fb06917a35c762 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Wed, 23 Apr 2025 09:52:45 +0200 Subject: [PATCH 11/24] Improve error handling and code structure --- public/i18n/en_US.json | 3 + src/config.ts | 31 ++++++---- src/editor.ts | 129 +++++++++++++++++++++++++---------------- src/errors.ts | 12 ++++ src/index.ts | 8 +-- 5 files changed, 116 insertions(+), 67 deletions(-) create mode 100644 src/errors.ts diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json index 88c96a3..a282081 100644 --- a/public/i18n/en_US.json +++ b/public/i18n/en_US.json @@ -2,6 +2,9 @@ "insertDrawing": "Insert Drawing", "editDrawing": "Edit with js-draw", "errNoFileID": "File ID missing - couldn't open file.", + "errSyncIDNotFound": "Couldn't find SyncID in document for drawing, make sure you're trying to edit a drawing that is included in at least a note.", + "errCreateUnknown": "Unknown error while creating editor, please try again.", + "errInvalidBackgroundColor": "Invalid background color! Please enter an HEX color, like #000000 (black) or #FFFFFF (white). The old background color will be used.", "drawing": "Drawing", "settings": { "name": "js-draw Plugin Settings", diff --git a/src/config.ts b/src/config.ts index 4491329..59842eb 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,6 @@ import {PluginFile} from "@/file"; import {CONFIG_FILENAME, JSON_MIME, STORAGE_PATH} from "@/const"; -import {Plugin} from "siyuan"; +import {Plugin, showMessage} from "siyuan"; import {SettingUtils} from "@/libs/setting-utils"; import {validateColor} from "@/helper"; @@ -61,10 +61,6 @@ export class PluginConfig { } setConfig(config: Options) { - if(!validateColor(config.background)) { - alert("Invalid background color! Please enter an HEX color, like #000000 (black) or #FFFFFF (white)"); - config.background = this.options.background; - } this.options = config; } @@ -83,19 +79,30 @@ export class PluginConfigViewer { this.populateSettingMenu(); } + async configSaveCallback(data) { + + if(!validateColor(data.background)) { + showMessage(this.plugin.i18n.errInvalidBackgroundColor, 0, 'error'); + data.background = this.config.options.background; + this.settingUtils.set('background', data.background); + } + this.config.setConfig({ + grid: data.grid, + background: data.background, + dialogOnDesktop: data.dialogOnDesktop, + analytics: data.analytics, + }); + await this.config.save(); + + } + populateSettingMenu() { this.settingUtils = new SettingUtils({ plugin: this.plugin, name: this.plugin.i18n.settings.name, callback: async (data) => { - this.config.setConfig({ - grid: data.grid, - background: data.background, - dialogOnDesktop: data.dialogOnDesktop, - analytics: data.analytics, - }); - await this.config.save(); + await this.configSaveCallback(data); } }); diff --git a/src/editor.ts b/src/editor.ts index d8bb16e..ca4914b 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -2,11 +2,12 @@ import {MaterialIconProvider} from "@js-draw/material-icons"; import {PluginAsset, PluginFile} from "@/file"; import {JSON_MIME, STORAGE_PATH, SVG_MIME, TOOLBAR_FILENAME} from "@/const"; import Editor, {BackgroundComponentBackgroundType, BaseWidget, Color4, EditorEventType} from "js-draw"; -import {Dialog, getFrontend, openTab, Plugin} from "siyuan"; +import {Dialog, getFrontend, openTab, Plugin, showMessage} from "siyuan"; import {findSyncIDInProtyle, replaceSyncID} from "@/protyle"; import DrawJSPlugin from "@/index"; import {DefaultEditorOptions} from "@/config"; import 'js-draw/styles'; +import {SyncIDNotFoundError, UnchangedProtyleError} from "@/errors"; export class PluginEditor { @@ -23,8 +24,9 @@ export class PluginEditor { getEditor(): Editor { return this.editor; } getFileID(): string { return this.fileID; } getSyncID(): string { return this.syncID; } + setSyncID(syncID: string) { this.syncID = syncID; } - constructor(fileID: string, defaultEditorOptions: DefaultEditorOptions) { + private constructor(fileID: string) { this.fileID = fileID; @@ -34,55 +36,56 @@ export class PluginEditor { iconProvider: new MaterialIconProvider(), }); - this.genToolbar().then(() => { - this.editor.dispatch(this.editor.setBackgroundStyle({ autoresize: true }), false); - this.editor.getRootElement().style.height = '100%'; - }); - - findSyncIDInProtyle(this.fileID).then(async (syncID) => { - - if(syncID == null) { - alert( - "Couldn't find SyncID in protyle for this file.\n" + - "Make sure the drawing you're trying to edit exists in a note.\n" + - "Close this editor tab now, and try to open the editor again." - ); - return; - } - - this.syncID = syncID; - // restore drawing - this.drawingFile = new PluginAsset(this.fileID, syncID, SVG_MIME); - await this.drawingFile.loadFromSiYuanFS(); - - if(this.drawingFile.getContent() != null) { - await this.editor.loadFromSVG(this.drawingFile.getContent()); - }else{ - // it's a new drawing - this.editor.dispatch(this.editor.setBackgroundStyle({ - color: Color4.fromHex(defaultEditorOptions.background), - type: defaultEditorOptions.grid ? BackgroundComponentBackgroundType.Grid : BackgroundComponentBackgroundType.SolidColor, - autoresize: true - })); - } - - }).catch((error) => { - alert("Error loading drawing: " + error); - }); + this.editor.dispatch(this.editor.setBackgroundStyle({ autoresize: true }), false); + this.editor.getRootElement().style.height = '100%'; } - private async genToolbar() { + static async create(fileID: string, defaultEditorOptions: DefaultEditorOptions): Promise { + + const instance = new PluginEditor(fileID); + + await instance.genToolbar(); + let syncID = await findSyncIDInProtyle(fileID); + + if(syncID == null) { + throw new SyncIDNotFoundError(fileID); + } + instance.setSyncID(syncID); + await instance.restoreOrInitFile(defaultEditorOptions); + + return instance; + + } + + async restoreOrInitFile(defaultEditorOptions: DefaultEditorOptions) { + + this.drawingFile = new PluginAsset(this.fileID, this.syncID, SVG_MIME); + await this.drawingFile.loadFromSiYuanFS(); + + if(this.drawingFile.getContent() != null) { + await this.editor.loadFromSVG(this.drawingFile.getContent()); + }else{ + // it's a new drawing + this.editor.dispatch(this.editor.setBackgroundStyle({ + color: Color4.fromHex(defaultEditorOptions.background), + type: defaultEditorOptions.grid ? BackgroundComponentBackgroundType.Grid : BackgroundComponentBackgroundType.SolidColor, + autoresize: true + })); + } + + } + + async genToolbar() { const toolbar = this.editor.addToolbar(); // restore toolbarFile state this.toolbarFile = new PluginFile(STORAGE_PATH, TOOLBAR_FILENAME, JSON_MIME); - this.toolbarFile.loadFromSiYuanFS().then(() => { - if(this.toolbarFile.getContent() != null) { - toolbar.deserializeState(this.toolbarFile.getContent()); - } - }); + await this.toolbarFile.loadFromSiYuanFS(); + if(this.toolbarFile.getContent() != null) { + toolbar.deserializeState(this.toolbarFile.getContent()); + } // save button const saveButton = toolbar.addSaveButton(async () => { @@ -109,7 +112,7 @@ export class PluginEditor { newSyncID = this.drawingFile.getSyncID(); if(newSyncID != oldSyncID) { // supposed to replace protyle const changed = await replaceSyncID(this.fileID, oldSyncID, newSyncID); // try to change protyle - if(!changed) throw new Error("Couldn't replace old images in protyle"); + if(!changed) throw new UnchangedProtyleError(); await this.drawingFile.removeOld(oldSyncID); } saveButton.setDisabled(true); @@ -117,7 +120,10 @@ export class PluginEditor { saveButton.setDisabled(false); }, 500); } catch (error) { - alert("Error saving! The current drawing has been copied to your clipboard. You may need to create a new drawing and paste it there."); + showMessage("Error saving! The current drawing has been copied to your clipboard. You may need to create a new drawing and paste it there.", 0, 'error'); + if(error instanceof UnchangedProtyleError) { + showMessage("Make sure the image you're trying to edit still exists in your documents.", 0, 'error'); + } await navigator.clipboard.writeText(svgElem.outerHTML); console.error(error); console.log("Couldn't save SVG: ", svgElem.outerHTML) @@ -133,26 +139,47 @@ export class PluginEditor { export class EditorManager { private editor: PluginEditor + setEditor(editor: PluginEditor) { this.editor = editor;} - constructor(fileID: string, defaultEditorOptions: DefaultEditorOptions) { - this.editor = new PluginEditor(fileID, defaultEditorOptions); + static async create(fileID: string, p: DrawJSPlugin) { + let instance = new EditorManager(); + try { + let editor = await PluginEditor.create(fileID, p.config.getDefaultEditorOptions()); + instance.setEditor(editor); + }catch (error) { + EditorManager.handleCreationError(error, p); + } + return instance; } static registerTab(p: DrawJSPlugin) { p.addTab({ 'type': "whiteboard", - init() { + async init() { const fileID = this.data.fileID; if (fileID == null) { alert(p.i18n.errNoFileID); return; } - const editor = new PluginEditor(fileID, p.config.getDefaultEditorOptions()); - this.element.appendChild(editor.getElement()); + try { + const editor = await PluginEditor.create(fileID, p.config.getDefaultEditorOptions()); + this.element.appendChild(editor.getElement()); + }catch (error){ + EditorManager.handleCreationError(error, p); + } } }); } + static handleCreationError(error: any, p: DrawJSPlugin) { + console.error(error); + let errorTxt = p.i18n.errCreateUnknown; + if(error instanceof SyncIDNotFoundError) { + errorTxt = p.i18n.errSyncIDNotFound; + } + showMessage(errorTxt, 0, 'error'); + } + toTab(p: Plugin) { openTab({ app: p.app, @@ -176,7 +203,7 @@ export class EditorManager { dialog.element.querySelector("#DrawingPanel").appendChild(this.editor.getElement()); } - async open(p: DrawJSPlugin) { + open(p: DrawJSPlugin) { if(getFrontend() != "mobile" && !p.config.options.dialogOnDesktop) { this.toTab(p); } else { diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..914bd9c --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,12 @@ + +export class SyncIDNotFoundError extends Error { + readonly fileID: string; + + constructor(fileID: string) { + super(`SyncID not found for file ${fileID}`); + this.fileID = fileID; + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export class UnchangedProtyleError extends Error {} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 1ef36aa..9d02d6d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,12 +29,12 @@ export default class DrawJSPlugin extends Plugin { id: "insert-drawing", filter: ["Insert Drawing", "Add drawing", "whiteboard", "freehand", "graphics", "jsdraw"], html: getMenuHTML("iconDraw", this.i18n.insertDrawing), - callback: (protyle: Protyle) => { + callback: async (protyle: Protyle) => { void this.analytics.sendEvent('create'); const fileID = generateRandomString(); const syncID = generateTimeString() + '-' + generateRandomString(); protyle.insert(getMarkdownBlock(fileID, syncID), true, false); - new EditorManager(fileID, this.config.getDefaultEditorOptions()).open(this); + (await EditorManager.create(fileID, this)).open(this); } }]; @@ -44,9 +44,9 @@ export default class DrawJSPlugin extends Plugin { e.detail.menu.addItem({ icon: "iconDraw", label: this.i18n.editDrawing, - click: () => { + click: async () => { void this.analytics.sendEvent('edit'); - new EditorManager(ids.fileID, this.config.getDefaultEditorOptions()).open(this); + (await EditorManager.create(ids.fileID, this)).open(this); } }) }) From 1ad26d1e2329fdfe6120adea1d87a1a64f105eda Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Thu, 1 May 2025 23:01:55 +0200 Subject: [PATCH 12/24] Add funding link --- plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.json b/plugin.json index 3f18451..b2832fc 100644 --- a/plugin.json +++ b/plugin.json @@ -31,7 +31,7 @@ }, "funding": { "custom": [ - "" + "https://s.massive.box/jsdraw-plugin-donate" ] }, "keywords": [ From fa3eba219e55c04d47bda16c1cbac8da59c19db4 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Mon, 5 May 2025 19:17:59 +0200 Subject: [PATCH 13/24] Suggest popular background colors, add transparency support Making the UI more user-friendly by suggesting some commonly used colors in the Settings menu --- public/i18n/en_US.json | 16 ++++++++++++++-- src/config.ts | 40 +++++++++++++++++++++++++++++++++------- src/editor.ts | 10 +++++----- src/helper.ts | 7 ------- 4 files changed, 52 insertions(+), 21 deletions(-) diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json index a282081..aa868b1 100644 --- a/public/i18n/en_US.json +++ b/public/i18n/en_US.json @@ -8,13 +8,25 @@ "drawing": "Drawing", "settings": { "name": "js-draw Plugin Settings", + "suggestedColors":{ + "white": "White", + "black": "Black", + "transparent": "Transparent", + "custom": "Custom", + "darkBlue": "Dark Blue", + "darkGray": "Dark Gray" + }, "grid": { "title": "Enable grid by default", "description": "Enable to automatically turn on the grid on new drawings." }, + "backgroundDropdown":{ + "title": "Background color", + "description": "Default background color for new drawings." + }, "background": { - "title": "Default background Color", - "description": "Default background color for new drawings, in hexadecimal." + "title": "Custom background", + "description": "Hexadecimal code of the custom background color for new drawings.
This setting is only applied if \"Background Color\" is set to \"Custom\"!" }, "dialogOnDesktop": { "title": "Open editor as dialog on desktop", diff --git a/src/config.ts b/src/config.ts index 59842eb..dd1c3ea 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,7 +2,6 @@ import {PluginFile} from "@/file"; import {CONFIG_FILENAME, JSON_MIME, STORAGE_PATH} from "@/const"; import {Plugin, showMessage} from "siyuan"; import {SettingUtils} from "@/libs/setting-utils"; -import {validateColor} from "@/helper"; type Options = { grid: boolean @@ -48,7 +47,7 @@ export class PluginConfig { private loadDefaultConfig() { this.options = { grid: true, - background: "#000000", + background: "#00000000", dialogOnDesktop: false, analytics: true, }; @@ -61,10 +60,16 @@ export class PluginConfig { } setConfig(config: Options) { - this.options = config; } + static validateColor(hex: string) { + hex = hex.replace('#', ''); + return typeof hex === 'string' + && (hex.length === 6 || hex.length === 8) + && !isNaN(Number('0x' + hex)) + } + } export class PluginConfigViewer { @@ -72,23 +77,34 @@ export class PluginConfigViewer { config: PluginConfig; settingUtils: SettingUtils; plugin: Plugin; + private readonly backgroundDropdownOptions; constructor(config: PluginConfig, plugin: Plugin) { this.config = config; this.plugin = plugin; + this.backgroundDropdownOptions = { + '#00000000': plugin.i18n.settings.suggestedColors.transparent, + 'CUSTOM': plugin.i18n.settings.suggestedColors.custom, + '#ffffff': plugin.i18n.settings.suggestedColors.white, + '#1e2227': plugin.i18n.settings.suggestedColors.darkBlue, + '#1e1e1e': plugin.i18n.settings.suggestedColors.darkGray, + '#000000': plugin.i18n.settings.suggestedColors.black, + } this.populateSettingMenu(); } async configSaveCallback(data) { - if(!validateColor(data.background)) { + let color = data.backgroundDropdown === "CUSTOM" ? data.background : data.backgroundDropdown; + if(!PluginConfig.validateColor(color)) { showMessage(this.plugin.i18n.errInvalidBackgroundColor, 0, 'error'); data.background = this.config.options.background; this.settingUtils.set('background', data.background); } + this.config.setConfig({ grid: data.grid, - background: data.background, + background: color, dialogOnDesktop: data.dialogOnDesktop, analytics: data.analytics, }); @@ -100,7 +116,7 @@ export class PluginConfigViewer { this.settingUtils = new SettingUtils({ plugin: this.plugin, - name: this.plugin.i18n.settings.name, + name: 'optionsUI', callback: async (data) => { await this.configSaveCallback(data); } @@ -114,12 +130,22 @@ export class PluginConfigViewer { type: 'checkbox' }); + this.settingUtils.addItem({ + key: 'backgroundDropdown', + title: this.plugin.i18n.settings.backgroundDropdown.title, + description: this.plugin.i18n.settings.backgroundDropdown.description, + type: 'select', + value: this.config.options.background in this.backgroundDropdownOptions ? + this.config.options.background : 'CUSTOM', + options: this.backgroundDropdownOptions, + }); + this.settingUtils.addItem({ key: "background", title: this.plugin.i18n.settings.background.title, description: this.plugin.i18n.settings.background.description, value: this.config.options.background, - type: 'textarea', + type: 'textinput', }); this.settingUtils.addItem({ diff --git a/src/editor.ts b/src/editor.ts index ca4914b..804a1e1 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -80,6 +80,11 @@ export class PluginEditor { const toolbar = this.editor.addToolbar(); + // save button + const saveButton = toolbar.addSaveButton(async () => { + await this.saveCallback(saveButton); + }); + // restore toolbarFile state this.toolbarFile = new PluginFile(STORAGE_PATH, TOOLBAR_FILENAME, JSON_MIME); await this.toolbarFile.loadFromSiYuanFS(); @@ -87,11 +92,6 @@ export class PluginEditor { toolbar.deserializeState(this.toolbarFile.getContent()); } - // save button - const saveButton = toolbar.addSaveButton(async () => { - await this.saveCallback(saveButton); - }); - // save toolbar config on tool change (toolbar state is not saved in SVGs!) this.editor.notifier.on(EditorEventType.ToolUpdated, () => { this.toolbarFile.setContent(toolbar.serializeState()); diff --git a/src/helper.ts b/src/helper.ts index 5a08636..22c3e8c 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -105,11 +105,4 @@ export function imgSrcToIDs(imgSrc: string | null): { fileID: string; syncID: st return assetPathToIDs(imgSrc); -} - -export function validateColor(hex: string) { - hex = hex.replace('#', ''); - return typeof hex === 'string' - && hex.length === 6 - && !isNaN(Number('0x' + hex)) } \ No newline at end of file From 764f9fe5a450744ef8c34ed07e79c75e86967518 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Tue, 6 May 2025 18:19:18 +0200 Subject: [PATCH 14/24] Add option to remember editor position and zoom --- public/i18n/en_US.json | 4 ++++ src/config.ts | 15 ++++++++++++++- src/editor.ts | 35 ++++++++++++++++++++++++++++++++--- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json index aa868b1..fbeb088 100644 --- a/public/i18n/en_US.json +++ b/public/i18n/en_US.json @@ -35,6 +35,10 @@ "analytics": { "title": "Analytics", "description": "Enable to send anonymous usage data to the developer. Privacy Policy" + }, + "restorePosition": { + "title": "Remember editor position", + "description": "When enabled, the editor will remember the zoom factor and position, and it will restore them the next time you open the drawing." } } } \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index dd1c3ea..11f5c2b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,6 +4,7 @@ import {Plugin, showMessage} from "siyuan"; import {SettingUtils} from "@/libs/setting-utils"; type Options = { + restorePosition: boolean; grid: boolean background: string dialogOnDesktop: boolean @@ -11,6 +12,7 @@ type Options = { }; export type DefaultEditorOptions = { + restorePosition: boolean; grid: boolean background: string } @@ -30,8 +32,9 @@ export class PluginConfig { getDefaultEditorOptions(): DefaultEditorOptions { return { + restorePosition: this.options.restorePosition, grid: this.options.grid, - background: this.options.background, + background: this.options.background }; } @@ -50,6 +53,7 @@ export class PluginConfig { background: "#00000000", dialogOnDesktop: false, analytics: true, + restorePosition: true, }; this.firstRun = true; } @@ -107,6 +111,7 @@ export class PluginConfigViewer { background: color, dialogOnDesktop: data.dialogOnDesktop, analytics: data.analytics, + restorePosition: data.restorePosition, }); await this.config.save(); @@ -148,6 +153,14 @@ export class PluginConfigViewer { type: 'textinput', }); + this.settingUtils.addItem({ + key: "restorePosition", + title: this.plugin.i18n.settings.restorePosition.title, + description: this.plugin.i18n.settings.restorePosition.description, + value: this.config.options.restorePosition, + type: 'checkbox' + }); + this.settingUtils.addItem({ key: "dialogOnDesktop", title: this.plugin.i18n.settings.dialogOnDesktop.title, diff --git a/src/editor.ts b/src/editor.ts index 804a1e1..cba875d 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -1,7 +1,15 @@ import {MaterialIconProvider} from "@js-draw/material-icons"; import {PluginAsset, PluginFile} from "@/file"; import {JSON_MIME, STORAGE_PATH, SVG_MIME, TOOLBAR_FILENAME} from "@/const"; -import Editor, {BackgroundComponentBackgroundType, BaseWidget, Color4, EditorEventType} from "js-draw"; +import Editor, { + BackgroundComponentBackgroundType, + BaseWidget, + Color4, + EditorEventType, + Mat33, + Vec2, + Viewport +} from "js-draw"; import {Dialog, getFrontend, openTab, Plugin, showMessage} from "siyuan"; import {findSyncIDInProtyle, replaceSyncID} from "@/protyle"; import DrawJSPlugin from "@/index"; @@ -62,9 +70,26 @@ export class PluginEditor { this.drawingFile = new PluginAsset(this.fileID, this.syncID, SVG_MIME); await this.drawingFile.loadFromSiYuanFS(); + const drawingContent = this.drawingFile.getContent(); + + if(drawingContent != null) { + + await this.editor.loadFromSVG(drawingContent); + + // restore position and zoom + const svgElem = new DOMParser().parseFromString(drawingContent, SVG_MIME).documentElement; + const editorViewStr = svgElem.getAttribute('editorView'); + if(editorViewStr != null && defaultEditorOptions.restorePosition) { + try { + const [viewBoxOriginX, viewBoxOriginY, zoom] = editorViewStr.split(' ').map(x => parseFloat(x)); + this.editor.dispatch(Viewport.transformBy(Mat33.scaling2D(zoom))); + this.editor.dispatch(Viewport.transformBy(Mat33.translation(Vec2.of( + - viewBoxOriginX, + - viewBoxOriginY + )))); + }catch (e){} + } - if(this.drawingFile.getContent() != null) { - await this.editor.loadFromSVG(this.drawingFile.getContent()); }else{ // it's a new drawing this.editor.dispatch(this.editor.setBackgroundStyle({ @@ -106,6 +131,10 @@ export class PluginEditor { let newSyncID: string; const oldSyncID = this.syncID; + const rect = this.editor.viewport.visibleRect; + const zoom = this.editor.viewport.getScaleFactor(); + svgElem.setAttribute('editorView', `${rect.x} ${rect.y} ${zoom}`) + try { this.drawingFile.setContent(svgElem.outerHTML); await this.drawingFile.save(); From 77e8218d1f20f8fa684b32b614917075ca044ce1 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Tue, 6 May 2025 23:12:51 +0200 Subject: [PATCH 15/24] Config improvements and compatibility with old versions --- package.json | 3 ++- src/config.ts | 63 ++++++++++++++++++++++----------------------------- src/editor.ts | 10 ++++---- src/helper.ts | 8 +++++++ 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index f355588..fc0d354 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ }, "dependencies": { "@js-draw/material-icons": "^1.29.0", - "js-draw": "^1.29.0" + "js-draw": "^1.29.0", + "ts-serializable": "^4.2.0" } } diff --git a/src/config.ts b/src/config.ts index 11f5c2b..7c4bfca 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,16 +2,14 @@ import {PluginFile} from "@/file"; import {CONFIG_FILENAME, JSON_MIME, STORAGE_PATH} from "@/const"; import {Plugin, showMessage} from "siyuan"; import {SettingUtils} from "@/libs/setting-utils"; +import {getFirstDefined} from "@/helper"; -type Options = { - restorePosition: boolean; - grid: boolean - background: string +export interface Options { dialogOnDesktop: boolean analytics: boolean -}; - -export type DefaultEditorOptions = { + editorOptions: EditorOptions +} +export interface EditorOptions { restorePosition: boolean; grid: boolean background: string @@ -30,32 +28,23 @@ export class PluginConfig { this.file = new PluginFile(STORAGE_PATH, CONFIG_FILENAME, JSON_MIME); } - getDefaultEditorOptions(): DefaultEditorOptions { - return { - restorePosition: this.options.restorePosition, - grid: this.options.grid, - background: this.options.background - }; - } - async load() { this.firstRun = false; await this.file.loadFromSiYuanFS(); - this.options = JSON.parse(this.file.getContent()); - if(this.options == null) { - this.loadDefaultConfig(); + const jsonObj = JSON.parse(this.file.getContent()); + if(jsonObj == null) { + this.firstRun = true; } - } - - private loadDefaultConfig() { + // if more than one fallback, the intermediate ones are from a legacy config file version this.options = { - grid: true, - background: "#00000000", - dialogOnDesktop: false, - analytics: true, - restorePosition: true, + dialogOnDesktop: getFirstDefined(jsonObj?.dialogOnDesktop, false), + analytics: getFirstDefined(jsonObj?.analytics, true), + editorOptions: { + restorePosition: getFirstDefined(jsonObj?.editorOptions?.restorePosition, jsonObj?.restorePosition, true), + grid: getFirstDefined(jsonObj?.editorOptions?.grid, jsonObj?.grid, true), + background: getFirstDefined(jsonObj?.editorOptions?.background, jsonObj?.background, "#00000000") + }, }; - this.firstRun = true; } async save() { @@ -102,16 +91,18 @@ export class PluginConfigViewer { let color = data.backgroundDropdown === "CUSTOM" ? data.background : data.backgroundDropdown; if(!PluginConfig.validateColor(color)) { showMessage(this.plugin.i18n.errInvalidBackgroundColor, 0, 'error'); - data.background = this.config.options.background; + data.background = this.config.options.editorOptions.background; this.settingUtils.set('background', data.background); } this.config.setConfig({ - grid: data.grid, - background: color, dialogOnDesktop: data.dialogOnDesktop, analytics: data.analytics, - restorePosition: data.restorePosition, + editorOptions: { + grid: data.grid, + background: color, + restorePosition: data.restorePosition, + } }); await this.config.save(); @@ -131,7 +122,7 @@ export class PluginConfigViewer { key: "grid", title: this.plugin.i18n.settings.grid.title, description: this.plugin.i18n.settings.grid.description, - value: this.config.options.grid, + value: this.config.options.editorOptions.grid, type: 'checkbox' }); @@ -140,8 +131,8 @@ export class PluginConfigViewer { title: this.plugin.i18n.settings.backgroundDropdown.title, description: this.plugin.i18n.settings.backgroundDropdown.description, type: 'select', - value: this.config.options.background in this.backgroundDropdownOptions ? - this.config.options.background : 'CUSTOM', + value: this.config.options.editorOptions.background in this.backgroundDropdownOptions ? + this.config.options.editorOptions.background : 'CUSTOM', options: this.backgroundDropdownOptions, }); @@ -149,7 +140,7 @@ export class PluginConfigViewer { key: "background", title: this.plugin.i18n.settings.background.title, description: this.plugin.i18n.settings.background.description, - value: this.config.options.background, + value: this.config.options.editorOptions.background, type: 'textinput', }); @@ -157,7 +148,7 @@ export class PluginConfigViewer { key: "restorePosition", title: this.plugin.i18n.settings.restorePosition.title, description: this.plugin.i18n.settings.restorePosition.description, - value: this.config.options.restorePosition, + value: this.config.options.editorOptions.restorePosition, type: 'checkbox' }); diff --git a/src/editor.ts b/src/editor.ts index cba875d..feb7094 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -13,7 +13,7 @@ import Editor, { import {Dialog, getFrontend, openTab, Plugin, showMessage} from "siyuan"; import {findSyncIDInProtyle, replaceSyncID} from "@/protyle"; import DrawJSPlugin from "@/index"; -import {DefaultEditorOptions} from "@/config"; +import {EditorOptions} from "@/config"; import 'js-draw/styles'; import {SyncIDNotFoundError, UnchangedProtyleError} from "@/errors"; @@ -49,7 +49,7 @@ export class PluginEditor { } - static async create(fileID: string, defaultEditorOptions: DefaultEditorOptions): Promise { + static async create(fileID: string, defaultEditorOptions: EditorOptions): Promise { const instance = new PluginEditor(fileID); @@ -66,7 +66,7 @@ export class PluginEditor { } - async restoreOrInitFile(defaultEditorOptions: DefaultEditorOptions) { + async restoreOrInitFile(defaultEditorOptions: EditorOptions) { this.drawingFile = new PluginAsset(this.fileID, this.syncID, SVG_MIME); await this.drawingFile.loadFromSiYuanFS(); @@ -173,7 +173,7 @@ export class EditorManager { static async create(fileID: string, p: DrawJSPlugin) { let instance = new EditorManager(); try { - let editor = await PluginEditor.create(fileID, p.config.getDefaultEditorOptions()); + let editor = await PluginEditor.create(fileID, p.config.options.editorOptions); instance.setEditor(editor); }catch (error) { EditorManager.handleCreationError(error, p); @@ -191,7 +191,7 @@ export class EditorManager { return; } try { - const editor = await PluginEditor.create(fileID, p.config.getDefaultEditorOptions()); + const editor = await PluginEditor.create(fileID, p.config.options.editorOptions); this.element.appendChild(editor.getElement()); }catch (error){ EditorManager.handleCreationError(error, p); diff --git a/src/helper.ts b/src/helper.ts index 22c3e8c..7041ba5 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -105,4 +105,12 @@ export function imgSrcToIDs(imgSrc: string | null): { fileID: string; syncID: st return assetPathToIDs(imgSrc); +} + +export function getFirstDefined(...a) { + for(let i = 0; i < a.length; i++) { + if(a[i] !== undefined) { + return a[i]; + } + } } \ No newline at end of file From 5322944ad9c2bc93c54a58e854292dd17fd7bdf9 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Wed, 7 May 2025 21:16:50 +0200 Subject: [PATCH 16/24] Add custom cursor on editor canvas --- public/webapp/cursor.png | Bin 0 -> 719 bytes src/editor.ts | 8 ++++++++ 2 files changed, 8 insertions(+) create mode 100644 public/webapp/cursor.png diff --git a/public/webapp/cursor.png b/public/webapp/cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..1306cf30de52d1f03364dd87a0c10082ae02e3d9 GIT binary patch literal 719 zcmV;=0xEX>4Tx04R}tkv&L4Q5c4wdo7GmByxyoxI;DNQW1@1fdmC&1!@i5A2q>ylY6hU zI0mgkLqAJ@LrZh54Xr^C^aIfzG!?ak+&C&Ik$uZ~c+dBKobNp#9~E@d>6il3a_vMc zsw7is#s5ktetLkAsyoH$czm9=^>vRV&bx@1)xPh~3K|(l2T1{@^rG#8v<6IUx<$cz zKv-W_4Uo=(5j&YmgY+nLt6p8YX@~6s8Z=A|4;81!dkam!KM~{f2=k08@8B6xBy% z-cSopbi=%i>M~M6@FbWyFmst;k+=TO8vQl;FonDBlV`!M`>5vVdoVH37%;(-TOdow z#gL0IVK+tcjB`^&HRs|EdPOn~lPRq;8Y8WX3IHN^NQChm>cap4002ovPDHLkV1m33 BHh};D literal 0 HcmV?d00001 diff --git a/src/editor.ts b/src/editor.ts index feb7094..1331e88 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -44,6 +44,14 @@ export class PluginEditor { iconProvider: new MaterialIconProvider(), }); + const styleElement = document.createElement('style'); + styleElement.innerHTML = ` + canvas.wetInkCanvas { + cursor: url('/plugins/siyuan-jsdraw-plugin/webapp/cursor.png') 6 6, auto; + } + `; + this.element.appendChild(styleElement); + this.editor.dispatch(this.editor.setBackgroundStyle({ autoresize: true }), false); this.editor.getRootElement().style.height = '100%'; From a079298433fefb6a795e5ecee959a293eec7736a Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Thu, 8 May 2025 22:47:09 +0200 Subject: [PATCH 17/24] Add CI --- .../workflows/build.yml | 37 ++++++++++++++----- scripts/validate_tag.cjs | 24 ++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) rename .github/workflows/release.yml => .forgejo/workflows/build.yml (60%) create mode 100644 scripts/validate_tag.cjs diff --git a/.github/workflows/release.yml b/.forgejo/workflows/build.yml similarity index 60% rename from .github/workflows/release.yml rename to .forgejo/workflows/build.yml index 49834e5..4bfcc57 100644 --- a/.github/workflows/release.yml +++ b/.forgejo/workflows/build.yml @@ -1,7 +1,9 @@ -name: Create Release on Tag Push +name: Build on Push and create Release on Tag on: push: + branches: + - main tags: - "v*" @@ -20,7 +22,7 @@ jobs: node-version: 20 registry-url: "https://registry.npmjs.org" - # Install pnpm + # Install pnpm - name: Install pnpm uses: pnpm/action-setup@v4 id: pnpm-install @@ -28,6 +30,12 @@ jobs: version: 8 run_install: false + # Validate Tag Matches JSON Versions + - name: Validate Tag Matches JSON Versions + if: github.ref_type == 'tag' + run: | + node scripts/validate_tag.cjs ${{ github.ref }} + # Get pnpm store directory - name: Get pnpm store directory id: pnpm-cache @@ -52,11 +60,22 @@ jobs: - name: Build for production run: pnpm build - - name: Release - uses: ncipollo/release-action@v1 + # Move file + - name: Move file + run: mkdir built; mv package.zip built/package.zip + + # Upload artifacts + - name: Upload artifacts + uses: actions/upload-artifact@v3 with: - allowUpdates: true - artifactErrorsFailBuild: true - artifacts: "package.zip" - token: ${{ secrets.GITHUB_TOKEN }} - prerelease: false + path: built/package.zip + overwrite: true + + # Create Forgejo Release + - name: Create Forgejo Release + if: github.ref_type == 'tag' + uses: actions/forgejo-release@v1 + with: + direction: upload + release-dir: built + token: ${{ secrets.FORGE_TOKEN }} diff --git a/scripts/validate_tag.cjs b/scripts/validate_tag.cjs new file mode 100644 index 0000000..c842ffc --- /dev/null +++ b/scripts/validate_tag.cjs @@ -0,0 +1,24 @@ +const fs = require('fs'); +const path = require('path'); + +const [tagName] = process.argv.slice(2); // Get tag from CLI arguments +if (!tagName) { + console.error('Error: No tag name provided.'); + process.exit(1); +} + +const TAG_VERSION = tagName.replace('refs/tags/v', ''); + +try { + const packageJson = JSON.parse(fs.readFileSync(path.resolve('package.json'), 'utf8')); + const pluginJson = JSON.parse(fs.readFileSync(path.resolve('plugin.json'), 'utf8')); + + if (TAG_VERSION !== packageJson.version || TAG_VERSION !== pluginJson.version) { + console.error(`Error: Tag version (${TAG_VERSION}) does not match package.json (${packageJson.version}) or plugin.json (${pluginJson.version})`); + process.exit(1); + } + console.log('Tag version matches both JSON files.'); +} catch (err) { + console.error('Failed to read or parse JSON files:', err.message); + process.exit(1); +} From 17d4e5938bba5a7b22d292a7f3d733d604f5d991 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Fri, 9 May 2025 22:57:42 +0200 Subject: [PATCH 18/24] Version bump --- package.json | 2 +- plugin.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fc0d354..76504eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "siyuan-jsdraw-plugin", - "version": "0.3.0", + "version": "0.4.0", "type": "module", "description": "Include a whiteboard for freehand drawing anywhere in your documents.", "repository": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin", diff --git a/plugin.json b/plugin.json index b2832fc..8acdb80 100644 --- a/plugin.json +++ b/plugin.json @@ -2,7 +2,7 @@ "name": "siyuan-jsdraw-plugin", "author": "massivebox", "url": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin", - "version": "0.3.0", + "version": "0.4.0", "minAppVersion": "3.0.12", "backends": [ "windows", From ff83c23851b23ce117d9a91dc77306ab90357076 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Thu, 15 May 2025 18:16:50 +0200 Subject: [PATCH 19/24] Improve cursor --- public/webapp/cursor.png | Bin 719 -> 606 bytes src/editor.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/public/webapp/cursor.png b/public/webapp/cursor.png index 1306cf30de52d1f03364dd87a0c10082ae02e3d9..939ddde36ccfabef2283bd91b20f706f8111ca36 100644 GIT binary patch delta 578 zcmV-I0=@mu1>OXIiBL{Q4GJ0x0000DNk~Le00006000065C#AM0I*y$3jhECeQ85O zP)S2WAaHVTW@&6?001bFeUUjx15p%3&rA%8L{gYS#9{=s(ndurH3~t95MqQxEu1AA zWX3Qv!6v0(8?bO`+zK0OZ7pmCL2v=F7pxR*B#itdkRZl?*Zjl%?>_#!ALOHgZaN(^ zU|OynkA#(EYDMXM#ZQnRgN&-WQ=E;(qO`5Adn9q*jfq+9`~IvRBkkxQDZq?gv|W(a zfvHWmD0mMD={eN^=^Plflc^Pu9)+&e`Au-P#$+kVAlyZDFm2^_*>jJP8aL zre%Pftg|71xGdN#l=Q|s#fE{jwY(&BV1Nh78_U-QQ( zp-~`@j>0?}n5Z>B;tzgj>trUz>yrXu@V|K8$1rs6K(*p|-^ZR;Jptb{Tv<*3#t}?^ zg0Irlh7KXP1sB&%P1}XbZRmR{s{_t2MWOdm8!u0Jq|D!VHT4^OLCoDt~oJL_t&-(_{Sh?f?J(|K<4^7#J8B z7>dInbW|_{3Rt!lLies>fM}SMdg#(A-c!8)|0ggdUrN4&pkd-Lbx;j&cEA)sOhV8w zahN(L1`+`a%#BGG(yi02bKMyX7%nkfLMVWUGcYhPFnnV8v}`RSsh$P^-G5%KIgh-T Q01E&B07*qoM6N<$g8TOTqW}N^ delta 691 zcmV;k0!;nh1kVM3iBL{Q4GJ0x0000DNk~Le0000C0000C5C#AM00!lNqyPW`d}%{N zP)S2WAaHVTW@&6?001bFeUUv#LQxoopL;EgP$Y7QXt+Z)LPI}Ge?v=itqrX~5cC7l9yArTgxokPD3N`C%XxUu_kNu3Js=+ybkpgW z0@HHsL@cT#Q)$KjN+*7LfRL&?#p!r_p0@RMk0j2!h?v#B@6QSv8Ak_60jBh#?Sixh zOl-PE!FxbhUsnx~&Vdm-nM#B7D0HpPZ-R3*mc`7ly_A>%ISeSd7S>u=U$;$=$ALk^ zv<#4wbv6Wlmj#=JlHPcy*f5Z>mKTK%3=m_E1>z`Zl*m)SB|;tx1&0JNqHX;BHGiBE z8U^y`D9o~fiCXg`{@{1EPIhdxJ}D4|&KJ-77y|zeR4bnMee8MF6YxF5mDTib9Kqx# z_$p0p@DM^KRFqG;mn0I|Npnk_PF=B z|Nn0$X;EfThM-~MFm*6}Fmo_m3{$Xv;x@%?ivOW5KOhRBgC{{~-a=FwCVoH^rVrI3 zeI6gE1!dkam!KM~{f2=k08@8B6xBy%-cSopbi=%i>M~M6@FbWyFmst;k+=TO8vQl; zFonDBlV`!M`>5vVdoVH37%;(-TOdow#gL0IVK+tcjB`^&HRs|EdPOn~lPRq;8Y8WX Z3IHN^NQChm>cap4002ovPDHLkV1l8aE}8%U diff --git a/src/editor.ts b/src/editor.ts index 1331e88..9ddb6de 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -47,7 +47,7 @@ export class PluginEditor { const styleElement = document.createElement('style'); styleElement.innerHTML = ` canvas.wetInkCanvas { - cursor: url('/plugins/siyuan-jsdraw-plugin/webapp/cursor.png') 6 6, auto; + cursor: url('/plugins/siyuan-jsdraw-plugin/webapp/cursor.png') 3 3, none; } `; this.element.appendChild(styleElement); From dc15e91def046c23c30ec6144f33a4140a9880e9 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Thu, 15 May 2025 18:19:46 +0200 Subject: [PATCH 20/24] Version bump --- package.json | 2 +- plugin.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 76504eb..b4b2293 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "siyuan-jsdraw-plugin", - "version": "0.4.0", + "version": "0.4.1", "type": "module", "description": "Include a whiteboard for freehand drawing anywhere in your documents.", "repository": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin", diff --git a/plugin.json b/plugin.json index 8acdb80..a5431f9 100644 --- a/plugin.json +++ b/plugin.json @@ -2,7 +2,7 @@ "name": "siyuan-jsdraw-plugin", "author": "massivebox", "url": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin", - "version": "0.4.0", + "version": "0.4.1", "minAppVersion": "3.0.12", "backends": [ "windows", From d34258e6bf825d1f525a4c05d3067597661a3e9b Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Tue, 24 Jun 2025 22:26:26 +0200 Subject: [PATCH 21/24] Add "directly open editor" shortcut and icon --- public/i18n/en_US.json | 2 ++ src/index.ts | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json index fbeb088..9372e17 100644 --- a/public/i18n/en_US.json +++ b/public/i18n/en_US.json @@ -1,10 +1,12 @@ { "insertDrawing": "Insert Drawing", "editDrawing": "Edit with js-draw", + "editShortcut": "Open editor directly", "errNoFileID": "File ID missing - couldn't open file.", "errSyncIDNotFound": "Couldn't find SyncID in document for drawing, make sure you're trying to edit a drawing that is included in at least a note.", "errCreateUnknown": "Unknown error while creating editor, please try again.", "errInvalidBackgroundColor": "Invalid background color! Please enter an HEX color, like #000000 (black) or #FFFFFF (white). The old background color will be used.", + "msgMustSelect": "Select a whiteboard in your document by left-clicking it, then use this icon/shortcut to open the editor directly.", "drawing": "Drawing", "settings": { "name": "js-draw Plugin Settings", diff --git a/src/index.ts b/src/index.ts index 9d02d6d..1d8fa92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import {Plugin, Protyle} from 'siyuan'; +import {Plugin, Protyle, showMessage} from 'siyuan'; import { getMarkdownBlock, loadIcons, @@ -51,6 +51,23 @@ export default class DrawJSPlugin extends Plugin { }) }) + this.addCommand({ + langKey: "editShortcut", + hotkey: "⌥⇧D", + callback: async () => { + await this.editSelectedImg(); + }, + }) + + this.addTopBar({ + icon: "iconDraw", + title: this.i18n.insertDrawing, + callback: async () => { + await this.editSelectedImg(); + }, + position: "left" + }) + } onunload() { @@ -61,6 +78,23 @@ export default class DrawJSPlugin extends Plugin { void this.analytics.sendEvent("uninstall"); } + private async editSelectedImg() { + + let selectedImg = document.getElementsByClassName('img--select'); + if(selectedImg.length == 0) { + showMessage(this.i18n.msgMustSelect, 5000, 'info'); + return; + } + + let ids = imgSrcToIDs(findImgSrc(selectedImg[0] as HTMLElement)); + if(ids == null) { + return; + } + void this.analytics.sendEvent('edit'); + (await EditorManager.create(ids.fileID, this)).open(this); + + } + private async startConfig() { this.config = new PluginConfig(); await this.config.load(); From 05984a89136463988195cc3e2dd142b27951628a Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Tue, 15 Jul 2025 12:42:18 +0200 Subject: [PATCH 22/24] Improve labels, errors, and docs --- README.md | 9 +++++---- public/i18n/en_US.json | 20 +++++++++++--------- src/editor.ts | 2 +- src/index.ts | 13 +++++++------ 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index f58614c..258f071 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,12 @@ This plugin allows you to embed js-draw whiteboards anywhere in your SiYuan docu ## Usage instructions - Install the plugin from the marketplace. You can find it by searching for `js-draw`. -- To add a new drawing to your document: - 1. Type `/Insert Drawing` in your document, and select the correct menu entry +- To add a new whiteboard to your document: + 1. Type `/Insert whiteboard` in your document, and select the correct menu entry 2. The whiteboard editor will open in a new tab. Draw as you like, then click the Save button and close the tab. -- To edit the image later: - 1. Right-click on the image (or click the three dots on mobile), select "Plugin" > "Edit with js-draw" in the menu +- To edit the whiteboard later: + 1. Left-click or tap on the whiteboard to select it, then click on the Edit icon in the top bar + - Or right-click on the whiteboard (or click the three dots on mobile), select "Plugin" > "Edit whiteboard" in the menu 2. The editor tab will open, edit your file as you like, then click the Save button and close the tab. ## Planned features diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json index 9372e17..494c8ac 100644 --- a/public/i18n/en_US.json +++ b/public/i18n/en_US.json @@ -1,13 +1,15 @@ { - "insertDrawing": "Insert Drawing", - "editDrawing": "Edit with js-draw", - "editShortcut": "Open editor directly", + "insertWhiteboard": "Insert whiteboard", + "editWhiteboard": "Edit whiteboard", + "editShortcut": "Edit selected whiteboard", "errNoFileID": "File ID missing - couldn't open file.", - "errSyncIDNotFound": "Couldn't find SyncID in document for drawing, make sure you're trying to edit a drawing that is included in at least a note.", + "errNotAWhiteboard": "You must select a whiteboard, not a regular image.", + "errSyncIDNotFound": "Couldn't find SyncID in document for drawing, make sure you're trying to edit a whiteboard that is included in at least a note.", "errCreateUnknown": "Unknown error while creating editor, please try again.", "errInvalidBackgroundColor": "Invalid background color! Please enter an HEX color, like #000000 (black) or #FFFFFF (white). The old background color will be used.", "msgMustSelect": "Select a whiteboard in your document by left-clicking it, then use this icon/shortcut to open the editor directly.", - "drawing": "Drawing", + "usageInstructionsLink": " Usage instructions", + "whiteboard": "Whiteboard", "settings": { "name": "js-draw Plugin Settings", "suggestedColors":{ @@ -20,15 +22,15 @@ }, "grid": { "title": "Enable grid by default", - "description": "Enable to automatically turn on the grid on new drawings." + "description": "Enable to automatically turn on the grid on new whiteboards." }, "backgroundDropdown":{ "title": "Background color", - "description": "Default background color for new drawings." + "description": "Default background color for new whiteboards." }, "background": { "title": "Custom background", - "description": "Hexadecimal code of the custom background color for new drawings.
This setting is only applied if \"Background Color\" is set to \"Custom\"!" + "description": "Hexadecimal code of the custom background color for new whiteboards.
This setting is only applied if \"Background Color\" is set to \"Custom\"!" }, "dialogOnDesktop": { "title": "Open editor as dialog on desktop", @@ -40,7 +42,7 @@ }, "restorePosition": { "title": "Remember editor position", - "description": "When enabled, the editor will remember the zoom factor and position, and it will restore them the next time you open the drawing." + "description": "When enabled, the editor will remember the zoom factor and position, and it will restore them the next time you open the same whiteboard." } } } \ No newline at end of file diff --git a/src/editor.ts b/src/editor.ts index 9ddb6de..7267593 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -221,7 +221,7 @@ export class EditorManager { openTab({ app: p.app, custom: { - title: p.i18n.drawing, + title: p.i18n.whiteboard, icon: 'iconDraw', id: "siyuan-jsdraw-pluginwhiteboard", data: { diff --git a/src/index.ts b/src/index.ts index 1d8fa92..9872ac7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,9 +26,9 @@ export default class DrawJSPlugin extends Plugin { await this.startAnalytics(); this.protyleSlash = [{ - id: "insert-drawing", - filter: ["Insert Drawing", "Add drawing", "whiteboard", "freehand", "graphics", "jsdraw"], - html: getMenuHTML("iconDraw", this.i18n.insertDrawing), + id: "insert-whiteboard", + filter: ["Insert Drawing", "Add drawing", "Insert whiteboard", "Add whiteboard", "whiteboard", "freehand", "graphics", "jsdraw"], + html: getMenuHTML("iconDraw", this.i18n.insertWhiteboard), callback: async (protyle: Protyle) => { void this.analytics.sendEvent('create'); const fileID = generateRandomString(); @@ -43,7 +43,7 @@ export default class DrawJSPlugin extends Plugin { if (ids === null) return; e.detail.menu.addItem({ icon: "iconDraw", - label: this.i18n.editDrawing, + label: this.i18n.editWhiteboard, click: async () => { void this.analytics.sendEvent('edit'); (await EditorManager.create(ids.fileID, this)).open(this); @@ -61,7 +61,7 @@ export default class DrawJSPlugin extends Plugin { this.addTopBar({ icon: "iconDraw", - title: this.i18n.insertDrawing, + title: this.i18n.editShortcut, callback: async () => { await this.editSelectedImg(); }, @@ -82,12 +82,13 @@ export default class DrawJSPlugin extends Plugin { let selectedImg = document.getElementsByClassName('img--select'); if(selectedImg.length == 0) { - showMessage(this.i18n.msgMustSelect, 5000, 'info'); + showMessage(this.i18n.msgMustSelect + this.i18n.usageInstructionsLink, 5000, 'info'); return; } let ids = imgSrcToIDs(findImgSrc(selectedImg[0] as HTMLElement)); if(ids == null) { + showMessage(this.i18n.errNotAWhiteboard + + this.i18n.usageInstructionsLink, 5000, 'error'); return; } void this.analytics.sendEvent('edit'); From eaf4a8e39e16777c4004f39959997b0e2934622f Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Wed, 16 Jul 2025 15:57:14 +0200 Subject: [PATCH 23/24] Improve error handling --- public/i18n/en_US.json | 8 +++-- src/config.ts | 5 +-- src/editor.ts | 34 ++++++++---------- src/errors.ts | 82 ++++++++++++++++++++++++++++++++++++++---- src/file.ts | 2 +- src/index.ts | 14 ++++---- src/protyle.ts | 9 ++--- 7 files changed, 109 insertions(+), 45 deletions(-) diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json index 494c8ac..1513654 100644 --- a/public/i18n/en_US.json +++ b/public/i18n/en_US.json @@ -3,12 +3,14 @@ "editWhiteboard": "Edit whiteboard", "editShortcut": "Edit selected whiteboard", "errNoFileID": "File ID missing - couldn't open file.", - "errNotAWhiteboard": "You must select a whiteboard, not a regular image.", + "errNotAWhiteboard": "You must select a whiteboard, not a regular image. Usage instructions", "errSyncIDNotFound": "Couldn't find SyncID in document for drawing, make sure you're trying to edit a whiteboard that is included in at least a note.", "errCreateUnknown": "Unknown error while creating editor, please try again.", "errInvalidBackgroundColor": "Invalid background color! Please enter an HEX color, like #000000 (black) or #FFFFFF (white). The old background color will be used.", - "msgMustSelect": "Select a whiteboard in your document by left-clicking it, then use this icon/shortcut to open the editor directly.", - "usageInstructionsLink": " Usage instructions", + "errMultipleSyncIDs": "Multiple syncIDs found in documents. Remove the drawings that don't exist from your documents.\n Sync conflict copies can cause this error, so make sure to delete them.\nFile IDs (the part you can change in the Rename menu) must be unique across all documents.\nFull explanation", + "errUnchangedProtyle": "Make sure the image you're trying to edit still exists in your documents.", + "errSaveGeneric": "Error saving! The current drawing has been copied to your clipboard. You may need to create a new drawing and paste it there.", + "errMustSelect": "Select a whiteboard in your document by left-clicking it, then use this icon/shortcut to open the editor directly. Usage instructions", "whiteboard": "Whiteboard", "settings": { "name": "js-draw Plugin Settings", diff --git a/src/config.ts b/src/config.ts index 7c4bfca..116a1af 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,8 +1,9 @@ import {PluginFile} from "@/file"; import {CONFIG_FILENAME, JSON_MIME, STORAGE_PATH} from "@/const"; -import {Plugin, showMessage} from "siyuan"; +import {Plugin} from "siyuan"; import {SettingUtils} from "@/libs/setting-utils"; import {getFirstDefined} from "@/helper"; +import {ErrorReporter, InvalidBackgroundColorError} from "@/errors"; export interface Options { dialogOnDesktop: boolean @@ -90,7 +91,7 @@ export class PluginConfigViewer { let color = data.backgroundDropdown === "CUSTOM" ? data.background : data.backgroundDropdown; if(!PluginConfig.validateColor(color)) { - showMessage(this.plugin.i18n.errInvalidBackgroundColor, 0, 'error'); + ErrorReporter.error(new InvalidBackgroundColorError()); data.background = this.config.options.editorOptions.background; this.settingUtils.set('background', data.background); } diff --git a/src/editor.ts b/src/editor.ts index 7267593..8fbeefb 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -10,12 +10,16 @@ import Editor, { Vec2, Viewport } from "js-draw"; -import {Dialog, getFrontend, openTab, Plugin, showMessage} from "siyuan"; +import {Dialog, getFrontend, openTab, Plugin} from "siyuan"; import {findSyncIDInProtyle, replaceSyncID} from "@/protyle"; import DrawJSPlugin from "@/index"; import {EditorOptions} from "@/config"; import 'js-draw/styles'; -import {SyncIDNotFoundError, UnchangedProtyleError} from "@/errors"; +import { + ErrorReporter, + GenericSaveError, InternationalizedError, NoFileIDError, SyncIDNotFoundError, + UnchangedProtyleError +} from "@/errors"; export class PluginEditor { @@ -65,7 +69,7 @@ export class PluginEditor { let syncID = await findSyncIDInProtyle(fileID); if(syncID == null) { - throw new SyncIDNotFoundError(fileID); + throw new SyncIDNotFoundError(); } instance.setSyncID(syncID); await instance.restoreOrInitFile(defaultEditorOptions); @@ -157,12 +161,13 @@ export class PluginEditor { saveButton.setDisabled(false); }, 500); } catch (error) { - showMessage("Error saving! The current drawing has been copied to your clipboard. You may need to create a new drawing and paste it there.", 0, 'error'); - if(error instanceof UnchangedProtyleError) { - showMessage("Make sure the image you're trying to edit still exists in your documents.", 0, 'error'); + if(error instanceof InternationalizedError) { + ErrorReporter.error(error); + }else{ + ErrorReporter.error(new GenericSaveError()); + console.error(error); } await navigator.clipboard.writeText(svgElem.outerHTML); - console.error(error); console.log("Couldn't save SVG: ", svgElem.outerHTML) return; } @@ -184,7 +189,7 @@ export class EditorManager { let editor = await PluginEditor.create(fileID, p.config.options.editorOptions); instance.setEditor(editor); }catch (error) { - EditorManager.handleCreationError(error, p); + ErrorReporter.error(error); } return instance; } @@ -195,28 +200,19 @@ export class EditorManager { async init() { const fileID = this.data.fileID; if (fileID == null) { - alert(p.i18n.errNoFileID); + ErrorReporter.error(new NoFileIDError()); return; } try { const editor = await PluginEditor.create(fileID, p.config.options.editorOptions); this.element.appendChild(editor.getElement()); }catch (error){ - EditorManager.handleCreationError(error, p); + ErrorReporter.error(error); } } }); } - static handleCreationError(error: any, p: DrawJSPlugin) { - console.error(error); - let errorTxt = p.i18n.errCreateUnknown; - if(error instanceof SyncIDNotFoundError) { - errorTxt = p.i18n.errSyncIDNotFound; - } - showMessage(errorTxt, 0, 'error'); - } - toTab(p: Plugin) { openTab({ app: p.app, diff --git a/src/errors.ts b/src/errors.ts index 914bd9c..f7bf264 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,12 +1,80 @@ +import {showMessage} from "siyuan"; -export class SyncIDNotFoundError extends Error { - readonly fileID: string; +export class InternationalizedError extends Error { + readonly key: string; - constructor(fileID: string) { - super(`SyncID not found for file ${fileID}`); - this.fileID = fileID; - Object.setPrototypeOf(this, new.target.prototype); + constructor(key: string) { + super(key); + this.key = key; } } -export class UnchangedProtyleError extends Error {} \ No newline at end of file +export class ErrorReporter { + + static i18n: any; + + constructor(i18n: any) { + ErrorReporter.i18n = i18n; + } + + static error(err: Error, timeout?: number) { + console.error(err); + let errorTxt = err.message; + if(err instanceof InternationalizedError) { + errorTxt = ErrorReporter.i18n[err.key]; + } + if(!timeout) { + timeout = 0; + } + showMessage(errorTxt, timeout, 'error'); + } + +} + +export class SyncIDNotFoundError extends InternationalizedError { + constructor() { + super('errSyncIDNotFound'); + } +} + +export class UnchangedProtyleError extends InternationalizedError { + constructor() { + super('errUnchangedProtyle'); + } +} + +export class MultipleSyncIDsError extends InternationalizedError { + constructor() { + super('errMultipleSyncIDs'); + } +} + +export class GenericSaveError extends InternationalizedError { + constructor() { + super('errSaveGeneric'); + } +} + +export class NotAWhiteboardError extends InternationalizedError { + constructor() { + super('errNotAWhiteboard'); + } +} + +export class InvalidBackgroundColorError extends InternationalizedError { + constructor() { + super('errInvalidBackgroundColor'); + } +} + +export class NoFileIDError extends InternationalizedError { + constructor() { + super('errNoFileID'); + } +} + +export class MustSelectError extends InternationalizedError { + constructor() { + super('errMustSelect'); + } +} \ No newline at end of file diff --git a/src/file.ts b/src/file.ts index dc2a86c..a3a1a10 100644 --- a/src/file.ts +++ b/src/file.ts @@ -52,7 +52,7 @@ abstract class PluginFileBase { protected toFile(customFilename?: string): File { let filename = customFilename || this.fileName; const blob = new Blob([this.content], { type: this.mimeType }); - return new File([blob], filename, { type: this.mimeType }); + return new File([blob], filename, { type: this.mimeType, lastModified: Date.now() }); } } diff --git a/src/index.ts b/src/index.ts index 9872ac7..6dbee74 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import {Plugin, Protyle, showMessage} from 'siyuan'; +import {Plugin, Protyle} from 'siyuan'; import { getMarkdownBlock, loadIcons, @@ -10,6 +10,7 @@ import {migrate} from "@/migration"; import {EditorManager} from "@/editor"; import {PluginConfig, PluginConfigViewer} from "@/config"; import {Analytics} from "@/analytics"; +import {ErrorReporter, MustSelectError, NotAWhiteboardError} from "@/errors"; export default class DrawJSPlugin extends Plugin { @@ -18,6 +19,7 @@ export default class DrawJSPlugin extends Plugin { async onload() { + new ErrorReporter(this.i18n); loadIcons(this); EditorManager.registerTab(this); migrate() @@ -55,7 +57,7 @@ export default class DrawJSPlugin extends Plugin { langKey: "editShortcut", hotkey: "⌥⇧D", callback: async () => { - await this.editSelectedImg(); + this.editSelectedImg().catch(e => ErrorReporter.error(e, 5000)); }, }) @@ -63,7 +65,7 @@ export default class DrawJSPlugin extends Plugin { icon: "iconDraw", title: this.i18n.editShortcut, callback: async () => { - await this.editSelectedImg(); + await this.editSelectedImg().catch(e => ErrorReporter.error(e, 5000)); }, position: "left" }) @@ -82,14 +84,12 @@ export default class DrawJSPlugin extends Plugin { let selectedImg = document.getElementsByClassName('img--select'); if(selectedImg.length == 0) { - showMessage(this.i18n.msgMustSelect + this.i18n.usageInstructionsLink, 5000, 'info'); - return; + throw new MustSelectError(); } let ids = imgSrcToIDs(findImgSrc(selectedImg[0] as HTMLElement)); if(ids == null) { - showMessage(this.i18n.errNotAWhiteboard + + this.i18n.usageInstructionsLink, 5000, 'error'); - return; + throw new NotAWhiteboardError(); } void this.analytics.sendEvent('edit'); (await EditorManager.create(ids.fileID, this)).open(this); diff --git a/src/protyle.ts b/src/protyle.ts index 5c3e842..6812864 100644 --- a/src/protyle.ts +++ b/src/protyle.ts @@ -1,5 +1,6 @@ import {getBlockByID, sql, updateBlock} from "@/api"; import {assetPathToIDs, IDsToAssetPath} from "@/helper"; +import {MultipleSyncIDsError} from "@/errors"; export async function findSyncIDInProtyle(fileID: string, iter?: number): Promise { @@ -15,11 +16,7 @@ export async function findSyncIDInProtyle(fileID: string, iter?: number): Promis if(syncID == null) { syncID = ids.syncID; }else if(ids.syncID !== syncID) { - throw new Error( - "Multiple syncIDs found in documents. Remove the drawings that don't exist from your documents.\n" + - "Sync conflict copies can cause this error, so make sure to delete them, or at least the js-draw drawings they contain.\n" + - "File IDs must be unique. Close this editor tab now." - ); + throw new MultipleSyncIDsError(); } } } @@ -82,7 +79,7 @@ export async function replaceBlockContent( } function extractImageSourcesFromMarkdown(markdown: string, mustStartWith?: string) { - const imageRegex = /!\[.*?\]\((.*?)\)/g; // only get images + const imageRegex = /!\[.*?\]\(([^)\s]+)(?:\s+"[^"]+")?\)/g; // only get images return Array.from(markdown.matchAll(imageRegex)) .map(match => match[1]) .filter(source => source.startsWith(mustStartWith)) // discard other images From 387437882495126d4d831683cc3e0da1c0fe133b Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Thu, 17 Jul 2025 16:24:38 +0200 Subject: [PATCH 24/24] Dropped migration legacy code --- src/index.ts | 2 -- src/migration.ts | 65 ------------------------------------------------ 2 files changed, 67 deletions(-) delete mode 100644 src/migration.ts diff --git a/src/index.ts b/src/index.ts index 6dbee74..1e0d691 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,6 @@ import { findImgSrc, imgSrcToIDs, generateTimeString, generateRandomString } from "@/helper"; -import {migrate} from "@/migration"; import {EditorManager} from "@/editor"; import {PluginConfig, PluginConfigViewer} from "@/config"; import {Analytics} from "@/analytics"; @@ -22,7 +21,6 @@ export default class DrawJSPlugin extends Plugin { new ErrorReporter(this.i18n); loadIcons(this); EditorManager.registerTab(this); - migrate() await this.startConfig(); await this.startAnalytics(); diff --git a/src/migration.ts b/src/migration.ts deleted file mode 100644 index 2829aad..0000000 --- a/src/migration.ts +++ /dev/null @@ -1,65 +0,0 @@ -import {sql} from "@/api"; -import {PluginAsset, PluginFile} from "@/file"; -import {ASSETS_PATH, DATA_PATH, SVG_MIME} from "@/const"; -import {replaceBlockContent} from "@/protyle"; -import {generateRandomString, getMarkdownBlock} from "@/helper"; -import {Dialog} from "siyuan"; - -export async function migrate() { - - let blocks = await findEmbedBlocks(); - const found = blocks.length > 0; - - for(const block of blocks) { - const oldFileID = extractID(block.markdown); - if(oldFileID) { - const oldFile = new PluginFile(DATA_PATH + ASSETS_PATH, oldFileID + '.svg', SVG_MIME); - await oldFile.loadFromSiYuanFS(); - const newFile = new PluginAsset(generateRandomString(), oldFileID, SVG_MIME); - newFile.setContent(oldFile.getContent()); - await newFile.save(); - const newMarkdown = getMarkdownBlock(newFile.getFileID(), newFile.getSyncID()); - if(await replaceBlockContent(block.id, block.markdown, newMarkdown)) { - await oldFile.remove(); - } - } - } - - if(found) { - new Dialog({ - width: "90vw", - height: "90vh", - content: ` -