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