From f35342a791d92f80d34cbc4bcddee6fa85a8f694 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Sun, 20 Apr 2025 22:16:48 +0200 Subject: [PATCH 01/15] 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 02/15] 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 03/15] 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 04/15] 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 05/15] 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 06/15] 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 07/15] 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 08/15] 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 09/15] 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 10/15] 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 11/15] 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 12/15] 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 13/15] 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 14/15] 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 15/15] 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: ` -