From 8d1438de331a78519686ed53bceb30001000c8c7 Mon Sep 17 00:00:00 2001 From: MassiveBox <box@massivebox.net> Date: Tue, 1 Apr 2025 23:03:22 +0200 Subject: [PATCH 01/27] Offer to edit images Any SVG image in assets/ can now be edited with js-draw. It won't reload automatically (yet) --- public/webapp/button.js | 6 +- src/helper.ts | 35 +++- src/index.ts | 32 +-- ....timestamp-1743541342564-d66840ad6dd8b.mjs | 185 ++++++++++++++++++ 4 files changed, 240 insertions(+), 18 deletions(-) create mode 100644 vite.config.ts.timestamp-1743541342564-d66840ad6dd8b.mjs diff --git a/public/webapp/button.js b/public/webapp/button.js index 610799d..7e6a160 100644 --- a/public/webapp/button.js +++ b/public/webapp/button.js @@ -1,6 +1,9 @@ function copyEditLink(fileID) { navigator.clipboard.writeText(getEditLink(fileID)); } +function copyImageLink(fileID) { + navigator.clipboard.writeText(``); +} function refreshPage() { window.location.reload(); @@ -20,7 +23,7 @@ function addButton(document, fileID) { popupMenu.innerHTML = ` <button onclick="refreshPage()">Refresh</button> <button onclick="copyEditLink('${fileID}')">Copy Direct Edit Link</button> - + <button onclick="copyImageLink('${fileID}')">Copy Image Link</button> `; document.body.appendChild(popupMenu); @@ -31,6 +34,7 @@ function addButton(document, fileID) { document.body.addEventListener('mouseleave', () => { floatingButton.style.display = 'none'; + popupMenu.style.display = 'none'; }); // Toggle popup menu on button click diff --git a/src/helper.ts b/src/helper.ts index 18386c7..0492b07 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -54,4 +54,37 @@ export function getPreviewHTML(id: string): string { return ` <iframe src="${EMBED_PATH + id}&antiCache=0"></iframe> ` -} \ No newline at end of file +} + +// given a tag (such as a div) containing an image as a child at any level, return the src of the image +export function findImgSrc(element: HTMLElement): string | null { + // Base case: if current element is an image + if (element.tagName === 'IMG') { + const fullSrc = (element as HTMLImageElement).src; + // Extract the path after host:port using URL API + const url = new URL(fullSrc); + return url.pathname.startsWith('/assets/') + ? url.pathname.substring(1) // Remove leading slash + : null; + } + + // Recursively check children + if (element.children) { + for (const child of Array.from(element.children)) { + const src = findImgSrc(child as HTMLElement); + if (src) return src; + } + } + + return null; +} + +export function extractFileID(imgSrc: string | null): string | null { + if (!imgSrc) return null; + + const [pathPart] = imgSrc.split('?'); + // Match pattern: assets/{fileID}.svg + const match = pathPart.match(/^assets\/([^\/]+)\.svg$/i); + + return match?.[1] || null; +} diff --git a/src/index.ts b/src/index.ts index e19ecb2..1ea3b47 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,11 @@ import {Plugin, Protyle} from 'siyuan'; -import {getPreviewHTML, loadIcons, getMenuHTML, generateSiyuanId} from "@/helper"; +import {getPreviewHTML, loadIcons, getMenuHTML, generateSiyuanId, findImgSrc, extractFileID} from "@/helper"; import {createEditor, openEditorTab} from "@/editorTab"; export default class DrawJSPlugin extends Plugin { onload() { loadIcons(this); - //const id = Math.random().toString(36).substring(7); this.addTab({ 'type': "whiteboard", init() { @@ -25,20 +24,21 @@ export default class DrawJSPlugin extends Plugin { } }]; + this.eventBus.on("open-menu-image", (e: any) => { + const fileID = extractFileID(findImgSrc(e.detail.element)); + if(fileID === null) { + return; + } + console.log("got ID" + fileID); + e.detail.menu.addItem({ + icon: "iconDraw", + label: "Edit with js-draw", + click: () => { + openEditorTab(this, fileID); + } + }) + }) + } - onLayoutReady() { - // This function is automatically called when the layout is loaded. - } - - onunload() { - // This function is automatically called when the plugin is disabled. - } - - uninstall() { - // This function is automatically called when the plugin is uninstalled. - } - - - } \ No newline at end of file diff --git a/vite.config.ts.timestamp-1743541342564-d66840ad6dd8b.mjs b/vite.config.ts.timestamp-1743541342564-d66840ad6dd8b.mjs new file mode 100644 index 0000000..f2c6618 --- /dev/null +++ b/vite.config.ts.timestamp-1743541342564-d66840ad6dd8b.mjs @@ -0,0 +1,185 @@ +// vite.config.ts +import { resolve as resolve2 } from "path"; +import { defineConfig } from "file:///home/massive/Dev/siyuan-jsdraw-plugin/node_modules/vite/dist/node/index.js"; +import { viteStaticCopy } from "file:///home/massive/Dev/siyuan-jsdraw-plugin/node_modules/vite-plugin-static-copy/dist/index.js"; +import livereload from "file:///home/massive/Dev/siyuan-jsdraw-plugin/node_modules/rollup-plugin-livereload/dist/index.cjs.js"; +import { svelte } from "file:///home/massive/Dev/siyuan-jsdraw-plugin/node_modules/@sveltejs/vite-plugin-svelte/src/index.js"; +import zipPack from "file:///home/massive/Dev/siyuan-jsdraw-plugin/node_modules/vite-plugin-zip-pack/dist/esm/index.mjs"; +import fg from "file:///home/massive/Dev/siyuan-jsdraw-plugin/node_modules/fast-glob/out/index.js"; + +// yaml-plugin.js +import fs from "fs"; +import yaml from "file:///home/massive/Dev/siyuan-jsdraw-plugin/node_modules/js-yaml/dist/js-yaml.mjs"; +import { resolve } from "path"; +function vitePluginYamlI18n(options = {}) { + const DefaultOptions = { + inDir: "src/i18n", + outDir: "dist/i18n" + }; + const finalOptions = { ...DefaultOptions, ...options }; + return { + name: "vite-plugin-yaml-i18n", + buildStart() { + console.log("\u{1F308} Parse I18n: YAML to JSON.."); + const inDir = finalOptions.inDir; + const outDir = finalOptions.outDir; + if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir, { recursive: true }); + } + const files = fs.readdirSync(inDir); + for (const file of files) { + if (file.endsWith(".yaml") || file.endsWith(".yml")) { + console.log(`-- Parsing ${file}`); + const jsonFile = file.replace(/\.(yaml|yml)$/, ".json"); + if (files.includes(jsonFile)) { + console.log(`---- File ${jsonFile} already exists, skipping...`); + continue; + } + try { + const filePath = resolve(inDir, file); + const fileContents = fs.readFileSync(filePath, "utf8"); + const parsed = yaml.load(fileContents); + const jsonContent = JSON.stringify(parsed, null, 2); + const outputFilePath = resolve(outDir, file.replace(/\.(yaml|yml)$/, ".json")); + console.log(`---- Writing to ${outputFilePath}`); + fs.writeFileSync(outputFilePath, jsonContent); + } catch (error) { + this.error(`---- Error parsing YAML file ${file}: ${error.message}`); + } + } + } + } + }; +} + +// vite.config.ts +var __vite_injected_original_dirname = "/home/massive/Dev/siyuan-jsdraw-plugin"; +var env = process.env; +var isSrcmap = env.VITE_SOURCEMAP === "inline"; +var isDev = env.NODE_ENV === "development"; +var outputDir = isDev ? "dev" : "dist"; +console.log("isDev=>", isDev); +console.log("isSrcmap=>", isSrcmap); +console.log("outputDir=>", outputDir); +var vite_config_default = defineConfig({ + resolve: { + alias: { + "@": resolve2(__vite_injected_original_dirname, "src") + } + }, + plugins: [ + svelte(), + vitePluginYamlI18n({ + inDir: "public/i18n", + outDir: `${outputDir}/i18n` + }), + viteStaticCopy({ + targets: [ + { src: "./README*.md", dest: "./" }, + { src: "./plugin.json", dest: "./" }, + { src: "./preview.png", dest: "./" }, + { src: "./icon.png", dest: "./" } + ] + }) + ], + define: { + "process.env.DEV_MODE": JSON.stringify(isDev), + "process.env.NODE_ENV": JSON.stringify(env.NODE_ENV) + }, + build: { + outDir: outputDir, + emptyOutDir: false, + minify: true, + sourcemap: isSrcmap ? "inline" : false, + lib: { + entry: resolve2(__vite_injected_original_dirname, "src/index.ts"), + fileName: "index", + formats: ["cjs"] + }, + rollupOptions: { + plugins: [ + ...isDev ? [ + livereload(outputDir), + { + name: "watch-external", + async buildStart() { + const files = await fg([ + "public/i18n/**", + "./README*.md", + "./plugin.json" + ]); + for (let file of files) { + this.addWatchFile(file); + } + } + } + ] : [ + // Clean up unnecessary files under dist dir + cleanupDistFiles({ + patterns: ["i18n/*.yaml", "i18n/*.md"], + distDir: outputDir + }), + zipPack({ + inDir: "./dist", + outDir: "./", + outFileName: "package.zip" + }) + ] + ], + external: ["siyuan", "process"], + output: { + entryFileNames: "[name].js", + assetFileNames: (assetInfo) => { + if (assetInfo.name === "style.css") { + return "index.css"; + } + return assetInfo.name; + } + } + } + } +}); +function cleanupDistFiles(options) { + const { + patterns, + distDir + } = options; + return { + name: "rollup-plugin-cleanup", + enforce: "post", + writeBundle: { + sequential: true, + order: "post", + async handler() { + const fg2 = await import("file:///home/massive/Dev/siyuan-jsdraw-plugin/node_modules/fast-glob/out/index.js"); + const fs2 = await import("fs"); + const distPatterns = patterns.map((pat) => `${distDir}/${pat}`); + console.debug("Cleanup searching patterns:", distPatterns); + const files = await fg2.default(distPatterns, { + dot: true, + absolute: true, + onlyFiles: false + }); + for (const file of files) { + try { + if (fs2.default.existsSync(file)) { + const stat = fs2.default.statSync(file); + if (stat.isDirectory()) { + fs2.default.rmSync(file, { recursive: true }); + } else { + fs2.default.unlinkSync(file); + } + console.log(`Cleaned up: ${file}`); + } + } catch (error) { + console.error(`Failed to clean up ${file}:`, error); + } + } + } + } + }; +} +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiLCAieWFtbC1wbHVnaW4uanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvaG9tZS9tYXNzaXZlL0Rldi9zaXl1YW4tanNkcmF3LXBsdWdpblwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL2hvbWUvbWFzc2l2ZS9EZXYvc2l5dWFuLWpzZHJhdy1wbHVnaW4vdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL2hvbWUvbWFzc2l2ZS9EZXYvc2l5dWFuLWpzZHJhdy1wbHVnaW4vdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyByZXNvbHZlIH0gZnJvbSBcInBhdGhcIlxuaW1wb3J0IHsgZGVmaW5lQ29uZmlnLCBsb2FkRW52IH0gZnJvbSBcInZpdGVcIlxuaW1wb3J0IHsgdml0ZVN0YXRpY0NvcHkgfSBmcm9tIFwidml0ZS1wbHVnaW4tc3RhdGljLWNvcHlcIlxuaW1wb3J0IGxpdmVyZWxvYWQgZnJvbSBcInJvbGx1cC1wbHVnaW4tbGl2ZXJlbG9hZFwiXG5pbXBvcnQgeyBzdmVsdGUgfSBmcm9tIFwiQHN2ZWx0ZWpzL3ZpdGUtcGx1Z2luLXN2ZWx0ZVwiXG5pbXBvcnQgemlwUGFjayBmcm9tIFwidml0ZS1wbHVnaW4temlwLXBhY2tcIjtcbmltcG9ydCBmZyBmcm9tICdmYXN0LWdsb2InO1xuXG5pbXBvcnQgdml0ZVBsdWdpbllhbWxJMThuIGZyb20gJy4veWFtbC1wbHVnaW4nO1xuXG5jb25zdCBlbnYgPSBwcm9jZXNzLmVudjtcbmNvbnN0IGlzU3JjbWFwID0gZW52LlZJVEVfU09VUkNFTUFQID09PSAnaW5saW5lJztcbmNvbnN0IGlzRGV2ID0gZW52Lk5PREVfRU5WID09PSAnZGV2ZWxvcG1lbnQnO1xuXG5jb25zdCBvdXRwdXREaXIgPSBpc0RldiA/IFwiZGV2XCIgOiBcImRpc3RcIjtcblxuY29uc29sZS5sb2coXCJpc0Rldj0+XCIsIGlzRGV2KTtcbmNvbnNvbGUubG9nKFwiaXNTcmNtYXA9PlwiLCBpc1NyY21hcCk7XG5jb25zb2xlLmxvZyhcIm91dHB1dERpcj0+XCIsIG91dHB1dERpcik7XG5cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gICAgcmVzb2x2ZToge1xuICAgICAgICBhbGlhczoge1xuICAgICAgICAgICAgXCJAXCI6IHJlc29sdmUoX19kaXJuYW1lLCBcInNyY1wiKSxcbiAgICAgICAgfVxuICAgIH0sXG5cbiAgICBwbHVnaW5zOiBbXG4gICAgICAgIHN2ZWx0ZSgpLFxuXG4gICAgICAgIHZpdGVQbHVnaW5ZYW1sSTE4bih7XG4gICAgICAgICAgICBpbkRpcjogJ3B1YmxpYy9pMThuJyxcbiAgICAgICAgICAgIG91dERpcjogYCR7b3V0cHV0RGlyfS9pMThuYFxuICAgICAgICB9KSxcblxuICAgICAgICB2aXRlU3RhdGljQ29weSh7XG4gICAgICAgICAgICB0YXJnZXRzOiBbXG4gICAgICAgICAgICAgICAgeyBzcmM6IFwiLi9SRUFETUUqLm1kXCIsIGRlc3Q6IFwiLi9cIiB9LFxuICAgICAgICAgICAgICAgIHsgc3JjOiBcIi4vcGx1Z2luLmpzb25cIiwgZGVzdDogXCIuL1wiIH0sXG4gICAgICAgICAgICAgICAgeyBzcmM6IFwiLi9wcmV2aWV3LnBuZ1wiLCBkZXN0OiBcIi4vXCIgfSxcbiAgICAgICAgICAgICAgICB7IHNyYzogXCIuL2ljb24ucG5nXCIsIGRlc3Q6IFwiLi9cIiB9XG4gICAgICAgICAgICBdLFxuICAgICAgICB9KSxcblxuICAgIF0sXG5cbiAgICBkZWZpbmU6IHtcbiAgICAgICAgXCJwcm9jZXNzLmVudi5ERVZfTU9ERVwiOiBKU09OLnN0cmluZ2lmeShpc0RldiksXG4gICAgICAgIFwicHJvY2Vzcy5lbnYuTk9ERV9FTlZcIjogSlNPTi5zdHJpbmdpZnkoZW52Lk5PREVfRU5WKVxuICAgIH0sXG5cbiAgICBidWlsZDoge1xuICAgICAgICBvdXREaXI6IG91dHB1dERpcixcbiAgICAgICAgZW1wdHlPdXREaXI6IGZhbHNlLFxuICAgICAgICBtaW5pZnk6IHRydWUsXG4gICAgICAgIHNvdXJjZW1hcDogaXNTcmNtYXAgPyAnaW5saW5lJyA6IGZhbHNlLFxuXG4gICAgICAgIGxpYjoge1xuICAgICAgICAgICAgZW50cnk6IHJlc29sdmUoX19kaXJuYW1lLCBcInNyYy9pbmRleC50c1wiKSxcbiAgICAgICAgICAgIGZpbGVOYW1lOiBcImluZGV4XCIsXG4gICAgICAgICAgICBmb3JtYXRzOiBbXCJjanNcIl0sXG4gICAgICAgIH0sXG4gICAgICAgIHJvbGx1cE9wdGlvbnM6IHtcbiAgICAgICAgICAgIHBsdWdpbnM6IFtcbiAgICAgICAgICAgICAgICAuLi4oaXNEZXYgPyBbXG4gICAgICAgICAgICAgICAgICAgIGxpdmVyZWxvYWQob3V0cHV0RGlyKSxcbiAgICAgICAgICAgICAgICAgICAge1xuICAgICAgICAgICAgICAgICAgICAgICAgbmFtZTogJ3dhdGNoLWV4dGVybmFsJyxcbiAgICAgICAgICAgICAgICAgICAgICAgIGFzeW5jIGJ1aWxkU3RhcnQoKSB7XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgY29uc3QgZmlsZXMgPSBhd2FpdCBmZyhbXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICdwdWJsaWMvaTE4bi8qKicsXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICcuL1JFQURNRSoubWQnLFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnLi9wbHVnaW4uanNvbidcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBdKTtcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBmb3IgKGxldCBmaWxlIG9mIGZpbGVzKSB7XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMuYWRkV2F0Y2hGaWxlKGZpbGUpO1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgIF0gOiBbXG4gICAgICAgICAgICAgICAgICAgIC8vIENsZWFuIHVwIHVubmVjZXNzYXJ5IGZpbGVzIHVuZGVyIGRpc3QgZGlyXG4gICAgICAgICAgICAgICAgICAgIGNsZWFudXBEaXN0RmlsZXMoe1xuICAgICAgICAgICAgICAgICAgICAgICAgcGF0dGVybnM6IFsnaTE4bi8qLnlhbWwnLCAnaTE4bi8qLm1kJ10sXG4gICAgICAgICAgICAgICAgICAgICAgICBkaXN0RGlyOiBvdXRwdXREaXJcbiAgICAgICAgICAgICAgICAgICAgfSksXG4gICAgICAgICAgICAgICAgICAgIHppcFBhY2soe1xuICAgICAgICAgICAgICAgICAgICAgICAgaW5EaXI6ICcuL2Rpc3QnLFxuICAgICAgICAgICAgICAgICAgICAgICAgb3V0RGlyOiAnLi8nLFxuICAgICAgICAgICAgICAgICAgICAgICAgb3V0RmlsZU5hbWU6ICdwYWNrYWdlLnppcCdcbiAgICAgICAgICAgICAgICAgICAgfSlcbiAgICAgICAgICAgICAgICBdKVxuICAgICAgICAgICAgXSxcblxuICAgICAgICAgICAgZXh0ZXJuYWw6IFtcInNpeXVhblwiLCBcInByb2Nlc3NcIl0sXG5cbiAgICAgICAgICAgIG91dHB1dDoge1xuICAgICAgICAgICAgICAgIGVudHJ5RmlsZU5hbWVzOiBcIltuYW1lXS5qc1wiLFxuICAgICAgICAgICAgICAgIGFzc2V0RmlsZU5hbWVzOiAoYXNzZXRJbmZvKSA9PiB7XG4gICAgICAgICAgICAgICAgICAgIGlmIChhc3NldEluZm8ubmFtZSA9PT0gXCJzdHlsZS5jc3NcIikge1xuICAgICAgICAgICAgICAgICAgICAgICAgcmV0dXJuIFwiaW5kZXguY3NzXCJcbiAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgICAgICByZXR1cm4gYXNzZXRJbmZvLm5hbWVcbiAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgfSxcbiAgICAgICAgfSxcbiAgICB9XG59KTtcblxuXG4vKipcbiAqIENsZWFuIHVwIHNvbWUgZGlzdCBmaWxlcyBhZnRlciBjb21waWxlZFxuICogQGF1dGhvciBmcm9zdGltZVxuICogQHBhcmFtIG9wdGlvbnM6XG4gKiBAcmV0dXJucyBcbiAqL1xuZnVuY3Rpb24gY2xlYW51cERpc3RGaWxlcyhvcHRpb25zOiB7IHBhdHRlcm5zOiBzdHJpbmdbXSwgZGlzdERpcjogc3RyaW5nIH0pIHtcbiAgICBjb25zdCB7XG4gICAgICAgIHBhdHRlcm5zLFxuICAgICAgICBkaXN0RGlyXG4gICAgfSA9IG9wdGlvbnM7XG5cbiAgICByZXR1cm4ge1xuICAgICAgICBuYW1lOiAncm9sbHVwLXBsdWdpbi1jbGVhbnVwJyxcbiAgICAgICAgZW5mb3JjZTogJ3Bvc3QnLFxuICAgICAgICB3cml0ZUJ1bmRsZToge1xuICAgICAgICAgICAgc2VxdWVudGlhbDogdHJ1ZSxcbiAgICAgICAgICAgIG9yZGVyOiAncG9zdCcgYXMgJ3Bvc3QnLFxuICAgICAgICAgICAgYXN5bmMgaGFuZGxlcigpIHtcbiAgICAgICAgICAgICAgICBjb25zdCBmZyA9IGF3YWl0IGltcG9ydCgnZmFzdC1nbG9iJyk7XG4gICAgICAgICAgICAgICAgY29uc3QgZnMgPSBhd2FpdCBpbXBvcnQoJ2ZzJyk7XG4gICAgICAgICAgICAgICAgLy8gY29uc3QgcGF0aCA9IGF3YWl0IGltcG9ydCgncGF0aCcpO1xuXG4gICAgICAgICAgICAgICAgLy8gXHU0RjdGXHU3NTI4IGdsb2IgXHU4QkVEXHU2Q0Q1XHVGRjBDXHU3ODZFXHU0RkREXHU4MEZEXHU1MzM5XHU5MTREXHU1MjMwXHU2NTg3XHU0RUY2XG4gICAgICAgICAgICAgICAgY29uc3QgZGlzdFBhdHRlcm5zID0gcGF0dGVybnMubWFwKHBhdCA9PiBgJHtkaXN0RGlyfS8ke3BhdH1gKTtcbiAgICAgICAgICAgICAgICBjb25zb2xlLmRlYnVnKCdDbGVhbnVwIHNlYXJjaGluZyBwYXR0ZXJuczonLCBkaXN0UGF0dGVybnMpO1xuXG4gICAgICAgICAgICAgICAgY29uc3QgZmlsZXMgPSBhd2FpdCBmZy5kZWZhdWx0KGRpc3RQYXR0ZXJucywge1xuICAgICAgICAgICAgICAgICAgICBkb3Q6IHRydWUsXG4gICAgICAgICAgICAgICAgICAgIGFic29sdXRlOiB0cnVlLFxuICAgICAgICAgICAgICAgICAgICBvbmx5RmlsZXM6IGZhbHNlXG4gICAgICAgICAgICAgICAgfSk7XG5cbiAgICAgICAgICAgICAgICAvLyBjb25zb2xlLmluZm8oJ0ZpbGVzIHRvIGJlIGNsZWFuZWQgdXA6JywgZmlsZXMpO1xuXG4gICAgICAgICAgICAgICAgZm9yIChjb25zdCBmaWxlIG9mIGZpbGVzKSB7XG4gICAgICAgICAgICAgICAgICAgIHRyeSB7XG4gICAgICAgICAgICAgICAgICAgICAgICBpZiAoZnMuZGVmYXVsdC5leGlzdHNTeW5jKGZpbGUpKSB7XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgY29uc3Qgc3RhdCA9IGZzLmRlZmF1bHQuc3RhdFN5bmMoZmlsZSk7XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgaWYgKHN0YXQuaXNEaXJlY3RvcnkoKSkge1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBmcy5kZWZhdWx0LnJtU3luYyhmaWxlLCB7IHJlY3Vyc2l2ZTogdHJ1ZSB9KTtcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBmcy5kZWZhdWx0LnVubGlua1N5bmMoZmlsZSk7XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNvbnNvbGUubG9nKGBDbGVhbmVkIHVwOiAke2ZpbGV9YCk7XG4gICAgICAgICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgICAgIH0gY2F0Y2ggKGVycm9yKSB7XG4gICAgICAgICAgICAgICAgICAgICAgICBjb25zb2xlLmVycm9yKGBGYWlsZWQgdG8gY2xlYW4gdXAgJHtmaWxlfTpgLCBlcnJvcik7XG4gICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICB9O1xufVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvaG9tZS9tYXNzaXZlL0Rldi9zaXl1YW4tanNkcmF3LXBsdWdpblwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL2hvbWUvbWFzc2l2ZS9EZXYvc2l5dWFuLWpzZHJhdy1wbHVnaW4veWFtbC1wbHVnaW4uanNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL2hvbWUvbWFzc2l2ZS9EZXYvc2l5dWFuLWpzZHJhdy1wbHVnaW4veWFtbC1wbHVnaW4uanNcIjsvKlxuICogQ29weXJpZ2h0IChjKSAyMDI0IGJ5IGZyb3N0aW1lLiBBbGwgUmlnaHRzIFJlc2VydmVkLlxuICogQEF1dGhvciAgICAgICA6IGZyb3N0aW1lXG4gKiBARGF0ZSAgICAgICAgIDogMjAyNC0wNC0wNSAyMToyNzo1NVxuICogQEZpbGVQYXRoICAgICA6IC95YW1sLXBsdWdpbi5qc1xuICogQExhc3RFZGl0VGltZSA6IDIwMjQtMDQtMDUgMjI6NTM6MzRcbiAqIEBEZXNjcmlwdGlvbiAgOiBcdTUzQkJcdTU5QUVcdTczOUJcdTc2ODQganNvbiBcdTY4M0NcdTVGMEZcdUZGMENcdTYyMTFcdTVDMzFcdTY2MkZcdTg5ODFcdTc1MjggeWFtbCBcdTUxOTkgaTE4blxuICovXG4vLyBwbHVnaW5zL3ZpdGUtcGx1Z2luLXBhcnNlLXlhbWwuanNcbmltcG9ydCBmcyBmcm9tICdmcyc7XG5pbXBvcnQgeWFtbCBmcm9tICdqcy15YW1sJztcbmltcG9ydCB7IHJlc29sdmUgfSBmcm9tICdwYXRoJztcblxuZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gdml0ZVBsdWdpbllhbWxJMThuKG9wdGlvbnMgPSB7fSkge1xuICAgIC8vIERlZmF1bHQgb3B0aW9ucyB3aXRoIGEgZmFsbGJhY2tcbiAgICBjb25zdCBEZWZhdWx0T3B0aW9ucyA9IHtcbiAgICAgICAgaW5EaXI6ICdzcmMvaTE4bicsXG4gICAgICAgIG91dERpcjogJ2Rpc3QvaTE4bicsXG4gICAgfTtcblxuICAgIGNvbnN0IGZpbmFsT3B0aW9ucyA9IHsgLi4uRGVmYXVsdE9wdGlvbnMsIC4uLm9wdGlvbnMgfTtcblxuICAgIHJldHVybiB7XG4gICAgICAgIG5hbWU6ICd2aXRlLXBsdWdpbi15YW1sLWkxOG4nLFxuICAgICAgICBidWlsZFN0YXJ0KCkge1xuICAgICAgICAgICAgY29uc29sZS5sb2coJ1x1RDgzQ1x1REYwOCBQYXJzZSBJMThuOiBZQU1MIHRvIEpTT04uLicpO1xuICAgICAgICAgICAgY29uc3QgaW5EaXIgPSBmaW5hbE9wdGlvbnMuaW5EaXI7XG4gICAgICAgICAgICBjb25zdCBvdXREaXIgPSBmaW5hbE9wdGlvbnMub3V0RGlyXG5cbiAgICAgICAgICAgIGlmICghZnMuZXhpc3RzU3luYyhvdXREaXIpKSB7XG4gICAgICAgICAgICAgICAgZnMubWtkaXJTeW5jKG91dERpciwgeyByZWN1cnNpdmU6IHRydWUgfSk7XG4gICAgICAgICAgICB9XG5cbiAgICAgICAgICAgIC8vUGFyc2UgeWFtbCBmaWxlLCBvdXRwdXQgdG8ganNvblxuICAgICAgICAgICAgY29uc3QgZmlsZXMgPSBmcy5yZWFkZGlyU3luYyhpbkRpcik7XG4gICAgICAgICAgICBmb3IgKGNvbnN0IGZpbGUgb2YgZmlsZXMpIHtcbiAgICAgICAgICAgICAgICBpZiAoZmlsZS5lbmRzV2l0aCgnLnlhbWwnKSB8fCBmaWxlLmVuZHNXaXRoKCcueW1sJykpIHtcbiAgICAgICAgICAgICAgICAgICAgY29uc29sZS5sb2coYC0tIFBhcnNpbmcgJHtmaWxlfWApXG4gICAgICAgICAgICAgICAgICAgIC8vXHU2OEMwXHU2N0U1XHU2NjJGXHU1NDI2XHU2NzA5XHU1NDBDXHU1NDBEXHU3Njg0anNvblx1NjU4N1x1NEVGNlxuICAgICAgICAgICAgICAgICAgICBjb25zdCBqc29uRmlsZSA9IGZpbGUucmVwbGFjZSgvXFwuKHlhbWx8eW1sKSQvLCAnLmpzb24nKTtcbiAgICAgICAgICAgICAgICAgICAgaWYgKGZpbGVzLmluY2x1ZGVzKGpzb25GaWxlKSkge1xuICAgICAgICAgICAgICAgICAgICAgICAgY29uc29sZS5sb2coYC0tLS0gRmlsZSAke2pzb25GaWxlfSBhbHJlYWR5IGV4aXN0cywgc2tpcHBpbmcuLi5gKTtcbiAgICAgICAgICAgICAgICAgICAgICAgIGNvbnRpbnVlO1xuICAgICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgICAgIHRyeSB7XG4gICAgICAgICAgICAgICAgICAgICAgICBjb25zdCBmaWxlUGF0aCA9IHJlc29sdmUoaW5EaXIsIGZpbGUpO1xuICAgICAgICAgICAgICAgICAgICAgICAgY29uc3QgZmlsZUNvbnRlbnRzID0gZnMucmVhZEZpbGVTeW5jKGZpbGVQYXRoLCAndXRmOCcpO1xuICAgICAgICAgICAgICAgICAgICAgICAgY29uc3QgcGFyc2VkID0geWFtbC5sb2FkKGZpbGVDb250ZW50cyk7XG4gICAgICAgICAgICAgICAgICAgICAgICBjb25zdCBqc29uQ29udGVudCA9IEpTT04uc3RyaW5naWZ5KHBhcnNlZCwgbnVsbCwgMik7XG4gICAgICAgICAgICAgICAgICAgICAgICBjb25zdCBvdXRwdXRGaWxlUGF0aCA9IHJlc29sdmUob3V0RGlyLCBmaWxlLnJlcGxhY2UoL1xcLih5YW1sfHltbCkkLywgJy5qc29uJykpO1xuICAgICAgICAgICAgICAgICAgICAgICAgY29uc29sZS5sb2coYC0tLS0gV3JpdGluZyB0byAke291dHB1dEZpbGVQYXRofWApO1xuICAgICAgICAgICAgICAgICAgICAgICAgZnMud3JpdGVGaWxlU3luYyhvdXRwdXRGaWxlUGF0aCwganNvbkNvbnRlbnQpO1xuICAgICAgICAgICAgICAgICAgICB9IGNhdGNoIChlcnJvcikge1xuICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy5lcnJvcihgLS0tLSBFcnJvciBwYXJzaW5nIFlBTUwgZmlsZSAke2ZpbGV9OiAke2Vycm9yLm1lc3NhZ2V9YCk7XG4gICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgfTtcbn1cbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBb1MsU0FBUyxXQUFBQSxnQkFBZTtBQUM1VCxTQUFTLG9CQUE2QjtBQUN0QyxTQUFTLHNCQUFzQjtBQUMvQixPQUFPLGdCQUFnQjtBQUN2QixTQUFTLGNBQWM7QUFDdkIsT0FBTyxhQUFhO0FBQ3BCLE9BQU8sUUFBUTs7O0FDR2YsT0FBTyxRQUFRO0FBQ2YsT0FBTyxVQUFVO0FBQ2pCLFNBQVMsZUFBZTtBQUVULFNBQVIsbUJBQW9DLFVBQVUsQ0FBQyxHQUFHO0FBRXJELFFBQU0saUJBQWlCO0FBQUEsSUFDbkIsT0FBTztBQUFBLElBQ1AsUUFBUTtBQUFBLEVBQ1o7QUFFQSxRQUFNLGVBQWUsRUFBRSxHQUFHLGdCQUFnQixHQUFHLFFBQVE7QUFFckQsU0FBTztBQUFBLElBQ0gsTUFBTTtBQUFBLElBQ04sYUFBYTtBQUNULGNBQVEsSUFBSSxzQ0FBK0I7QUFDM0MsWUFBTSxRQUFRLGFBQWE7QUFDM0IsWUFBTSxTQUFTLGFBQWE7QUFFNUIsVUFBSSxDQUFDLEdBQUcsV0FBVyxNQUFNLEdBQUc7QUFDeEIsV0FBRyxVQUFVLFFBQVEsRUFBRSxXQUFXLEtBQUssQ0FBQztBQUFBLE1BQzVDO0FBR0EsWUFBTSxRQUFRLEdBQUcsWUFBWSxLQUFLO0FBQ2xDLGlCQUFXLFFBQVEsT0FBTztBQUN0QixZQUFJLEtBQUssU0FBUyxPQUFPLEtBQUssS0FBSyxTQUFTLE1BQU0sR0FBRztBQUNqRCxrQkFBUSxJQUFJLGNBQWMsSUFBSSxFQUFFO0FBRWhDLGdCQUFNLFdBQVcsS0FBSyxRQUFRLGlCQUFpQixPQUFPO0FBQ3RELGNBQUksTUFBTSxTQUFTLFFBQVEsR0FBRztBQUMxQixvQkFBUSxJQUFJLGFBQWEsUUFBUSw4QkFBOEI7QUFDL0Q7QUFBQSxVQUNKO0FBQ0EsY0FBSTtBQUNBLGtCQUFNLFdBQVcsUUFBUSxPQUFPLElBQUk7QUFDcEMsa0JBQU0sZUFBZSxHQUFHLGFBQWEsVUFBVSxNQUFNO0FBQ3JELGtCQUFNLFNBQVMsS0FBSyxLQUFLLFlBQVk7QUFDckMsa0JBQU0sY0FBYyxLQUFLLFVBQVUsUUFBUSxNQUFNLENBQUM7QUFDbEQsa0JBQU0saUJBQWlCLFFBQVEsUUFBUSxLQUFLLFFBQVEsaUJBQWlCLE9BQU8sQ0FBQztBQUM3RSxvQkFBUSxJQUFJLG1CQUFtQixjQUFjLEVBQUU7QUFDL0MsZUFBRyxjQUFjLGdCQUFnQixXQUFXO0FBQUEsVUFDaEQsU0FBUyxPQUFPO0FBQ1osaUJBQUssTUFBTSxnQ0FBZ0MsSUFBSSxLQUFLLE1BQU0sT0FBTyxFQUFFO0FBQUEsVUFDdkU7QUFBQSxRQUNKO0FBQUEsTUFDSjtBQUFBLElBQ0o7QUFBQSxFQUNKO0FBQ0o7OztBRDNEQSxJQUFNLG1DQUFtQztBQVV6QyxJQUFNLE1BQU0sUUFBUTtBQUNwQixJQUFNLFdBQVcsSUFBSSxtQkFBbUI7QUFDeEMsSUFBTSxRQUFRLElBQUksYUFBYTtBQUUvQixJQUFNLFlBQVksUUFBUSxRQUFRO0FBRWxDLFFBQVEsSUFBSSxXQUFXLEtBQUs7QUFDNUIsUUFBUSxJQUFJLGNBQWMsUUFBUTtBQUNsQyxRQUFRLElBQUksZUFBZSxTQUFTO0FBRXBDLElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQ3hCLFNBQVM7QUFBQSxJQUNMLE9BQU87QUFBQSxNQUNILEtBQUtDLFNBQVEsa0NBQVcsS0FBSztBQUFBLElBQ2pDO0FBQUEsRUFDSjtBQUFBLEVBRUEsU0FBUztBQUFBLElBQ0wsT0FBTztBQUFBLElBRVAsbUJBQW1CO0FBQUEsTUFDZixPQUFPO0FBQUEsTUFDUCxRQUFRLEdBQUcsU0FBUztBQUFBLElBQ3hCLENBQUM7QUFBQSxJQUVELGVBQWU7QUFBQSxNQUNYLFNBQVM7QUFBQSxRQUNMLEVBQUUsS0FBSyxnQkFBZ0IsTUFBTSxLQUFLO0FBQUEsUUFDbEMsRUFBRSxLQUFLLGlCQUFpQixNQUFNLEtBQUs7QUFBQSxRQUNuQyxFQUFFLEtBQUssaUJBQWlCLE1BQU0sS0FBSztBQUFBLFFBQ25DLEVBQUUsS0FBSyxjQUFjLE1BQU0sS0FBSztBQUFBLE1BQ3BDO0FBQUEsSUFDSixDQUFDO0FBQUEsRUFFTDtBQUFBLEVBRUEsUUFBUTtBQUFBLElBQ0osd0JBQXdCLEtBQUssVUFBVSxLQUFLO0FBQUEsSUFDNUMsd0JBQXdCLEtBQUssVUFBVSxJQUFJLFFBQVE7QUFBQSxFQUN2RDtBQUFBLEVBRUEsT0FBTztBQUFBLElBQ0gsUUFBUTtBQUFBLElBQ1IsYUFBYTtBQUFBLElBQ2IsUUFBUTtBQUFBLElBQ1IsV0FBVyxXQUFXLFdBQVc7QUFBQSxJQUVqQyxLQUFLO0FBQUEsTUFDRCxPQUFPQSxTQUFRLGtDQUFXLGNBQWM7QUFBQSxNQUN4QyxVQUFVO0FBQUEsTUFDVixTQUFTLENBQUMsS0FBSztBQUFBLElBQ25CO0FBQUEsSUFDQSxlQUFlO0FBQUEsTUFDWCxTQUFTO0FBQUEsUUFDTCxHQUFJLFFBQVE7QUFBQSxVQUNSLFdBQVcsU0FBUztBQUFBLFVBQ3BCO0FBQUEsWUFDSSxNQUFNO0FBQUEsWUFDTixNQUFNLGFBQWE7QUFDZixvQkFBTSxRQUFRLE1BQU0sR0FBRztBQUFBLGdCQUNuQjtBQUFBLGdCQUNBO0FBQUEsZ0JBQ0E7QUFBQSxjQUNKLENBQUM7QUFDRCx1QkFBUyxRQUFRLE9BQU87QUFDcEIscUJBQUssYUFBYSxJQUFJO0FBQUEsY0FDMUI7QUFBQSxZQUNKO0FBQUEsVUFDSjtBQUFBLFFBQ0osSUFBSTtBQUFBO0FBQUEsVUFFQSxpQkFBaUI7QUFBQSxZQUNiLFVBQVUsQ0FBQyxlQUFlLFdBQVc7QUFBQSxZQUNyQyxTQUFTO0FBQUEsVUFDYixDQUFDO0FBQUEsVUFDRCxRQUFRO0FBQUEsWUFDSixPQUFPO0FBQUEsWUFDUCxRQUFRO0FBQUEsWUFDUixhQUFhO0FBQUEsVUFDakIsQ0FBQztBQUFBLFFBQ0w7QUFBQSxNQUNKO0FBQUEsTUFFQSxVQUFVLENBQUMsVUFBVSxTQUFTO0FBQUEsTUFFOUIsUUFBUTtBQUFBLFFBQ0osZ0JBQWdCO0FBQUEsUUFDaEIsZ0JBQWdCLENBQUMsY0FBYztBQUMzQixjQUFJLFVBQVUsU0FBUyxhQUFhO0FBQ2hDLG1CQUFPO0FBQUEsVUFDWDtBQUNBLGlCQUFPLFVBQVU7QUFBQSxRQUNyQjtBQUFBLE1BQ0o7QUFBQSxJQUNKO0FBQUEsRUFDSjtBQUNKLENBQUM7QUFTRCxTQUFTLGlCQUFpQixTQUFrRDtBQUN4RSxRQUFNO0FBQUEsSUFDRjtBQUFBLElBQ0E7QUFBQSxFQUNKLElBQUk7QUFFSixTQUFPO0FBQUEsSUFDSCxNQUFNO0FBQUEsSUFDTixTQUFTO0FBQUEsSUFDVCxhQUFhO0FBQUEsTUFDVCxZQUFZO0FBQUEsTUFDWixPQUFPO0FBQUEsTUFDUCxNQUFNLFVBQVU7QUFDWixjQUFNQyxNQUFLLE1BQU0sT0FBTyxtRkFBVztBQUNuQyxjQUFNQyxNQUFLLE1BQU0sT0FBTyxJQUFJO0FBSTVCLGNBQU0sZUFBZSxTQUFTLElBQUksU0FBTyxHQUFHLE9BQU8sSUFBSSxHQUFHLEVBQUU7QUFDNUQsZ0JBQVEsTUFBTSwrQkFBK0IsWUFBWTtBQUV6RCxjQUFNLFFBQVEsTUFBTUQsSUFBRyxRQUFRLGNBQWM7QUFBQSxVQUN6QyxLQUFLO0FBQUEsVUFDTCxVQUFVO0FBQUEsVUFDVixXQUFXO0FBQUEsUUFDZixDQUFDO0FBSUQsbUJBQVcsUUFBUSxPQUFPO0FBQ3RCLGNBQUk7QUFDQSxnQkFBSUMsSUFBRyxRQUFRLFdBQVcsSUFBSSxHQUFHO0FBQzdCLG9CQUFNLE9BQU9BLElBQUcsUUFBUSxTQUFTLElBQUk7QUFDckMsa0JBQUksS0FBSyxZQUFZLEdBQUc7QUFDcEIsZ0JBQUFBLElBQUcsUUFBUSxPQUFPLE1BQU0sRUFBRSxXQUFXLEtBQUssQ0FBQztBQUFBLGNBQy9DLE9BQU87QUFDSCxnQkFBQUEsSUFBRyxRQUFRLFdBQVcsSUFBSTtBQUFBLGNBQzlCO0FBQ0Esc0JBQVEsSUFBSSxlQUFlLElBQUksRUFBRTtBQUFBLFlBQ3JDO0FBQUEsVUFDSixTQUFTLE9BQU87QUFDWixvQkFBUSxNQUFNLHNCQUFzQixJQUFJLEtBQUssS0FBSztBQUFBLFVBQ3REO0FBQUEsUUFDSjtBQUFBLE1BQ0o7QUFBQSxJQUNKO0FBQUEsRUFDSjtBQUNKOyIsCiAgIm5hbWVzIjogWyJyZXNvbHZlIiwgInJlc29sdmUiLCAiZmciLCAiZnMiXQp9Cg== From 56cf62f1eb480e2df1ac309c4bfd7a7f6ca4e312 Mon Sep 17 00:00:00 2001 From: MassiveBox <box@massivebox.net> Date: Tue, 1 Apr 2025 23:24:17 +0200 Subject: [PATCH 02/27] Improve docs --- README.md | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 73c10ff..c759374 100644 --- a/README.md +++ b/README.md @@ -4,26 +4,23 @@ This plugin allows you to embed js-draw whiteboards anywhere in your SiYuan documents. ## Usage instructions -1. Install the plugin - - Grab a release from the [Releases page](https://git.massive.box/massivebox/siyuan-jsdraw-plugin/releases) - - Unzip it in the folder `./data/plugins`, relatively to your SiYuan workspace. - > The plugin is not yet available in the official marketplace. I will try to publish it there soon! -2. Insert a drawing in your documents by typing `/Insert Drawing` in your document, and selecting the correct menu entry -3. The whiteboard editor will open in a new tab. Draw as you like, then click the Save button. It will also add a - drawing block to your document. -4. Click the Gear icon > Refresh to refresh the drawing block, if it's still displaying the old drawing. -5. Click the drawing block to open the editor again. +- Install the plugin from the marketplace. You can find it by searching for `js-draw`. +- To edit an SVG image that is already embedded in your document: + 1. Right-click on the image, select "Plugin" > "Edit with js-draw" in the menu + 2. The editor tab will open, edit your file as you like, then click the Save button and close the tab. + 3. The image is updated, but SiYuan will still show the cached (old) image. This will be fixed in future releases, + please be patient. Until them, you can refresh the editor or change the image path. +- To add a new drawing to your document: + 1. Type `/Insert Drawing` 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. + 3. Click the Gear icon > Refresh to refresh the drawing block. + 4. Click the drawing block to open the editor again. ## Planned features -- [ ] Auto-reload drawing blocks on drawing change -- [ ] Rename whiteboards -- [ ] Improve internationalization framework -- [ ] Default background color and grid options -- [ ] Respecting user theme for the editor -- And more! +Check out the [Projects](https://git.massive.box/massivebox/siyuan-jsdraw-plugin/projects) tab! ## Contributing -Contributions are always welcome! Right now, I'm working on the core functionality and fixing bugs. +Contributions are always welcome! Right now, I'm working on the core functionality and fixing bugs. After that is done, I will need help with the internationalization, as, unfortunately, I don't speak Chinese. Please [contact me](mailto:box@massive.box) if you'd like to help! From a2503d5defd00f5939778b0660ca1bd227a13ede Mon Sep 17 00:00:00 2001 From: MassiveBox <box@massivebox.net> Date: Wed, 2 Apr 2025 20:15:48 +0200 Subject: [PATCH 03/27] Move from file IDs to file paths (with retrocompatibility) --- public/webapp/button.js | 14 +++++++------- public/webapp/draw.js | 8 ++++---- public/webapp/index.html | 12 ++++++++---- src/const.ts | 4 ++-- src/editorTab.ts | 24 ++++++++++++++---------- src/helper.ts | 26 ++++++++++++-------------- src/index.ts | 23 +++++++++++++++-------- 7 files changed, 62 insertions(+), 49 deletions(-) diff --git a/public/webapp/button.js b/public/webapp/button.js index 7e6a160..5aa902d 100644 --- a/public/webapp/button.js +++ b/public/webapp/button.js @@ -1,15 +1,15 @@ -function copyEditLink(fileID) { - navigator.clipboard.writeText(getEditLink(fileID)); +function copyEditLink(path) { + navigator.clipboard.writeText(getEditLink(path)); } -function copyImageLink(fileID) { - navigator.clipboard.writeText(``); +function copyImageLink(path) { + navigator.clipboard.writeText(`})`); } function refreshPage() { window.location.reload(); } -function addButton(document, fileID) { +function addButton(document, path) { // Add floating button const floatingButton = document.createElement('button'); @@ -22,8 +22,8 @@ function addButton(document, fileID) { popupMenu.id = 'popupMenu'; popupMenu.innerHTML = ` <button onclick="refreshPage()">Refresh</button> - <button onclick="copyEditLink('${fileID}')">Copy Direct Edit Link</button> - <button onclick="copyImageLink('${fileID}')">Copy Image Link</button> + <button onclick="copyEditLink('${path}')">Copy Direct Edit Link</button> + <button onclick="copyImageLink('${path}')">Copy Image Link</button> `; document.body.appendChild(popupMenu); diff --git a/public/webapp/draw.js b/public/webapp/draw.js index 9d9adfc..baaf005 100644 --- a/public/webapp/draw.js +++ b/public/webapp/draw.js @@ -27,9 +27,9 @@ async function getFile(path) { } -async function getSVG(fileID) { +async function getSVG(path) { - const resp = await getFile("/data/assets/" + fileID + '.svg'); + const resp = await getFile(path); if(resp == null) { return FALLBACK; } @@ -37,10 +37,10 @@ async function getSVG(fileID) { } -function getEditLink(fileID) { +function getEditLink(path) { const data = encodeURIComponent( JSON.stringify({ - id: fileID + path: path, }) ) return `siyuan://plugins/siyuan-jsdraw-pluginwhiteboard/?icon=iconDraw&title=Drawing&data=${data}`; diff --git a/public/webapp/index.html b/public/webapp/index.html index a1fd2f6..c40fb79 100644 --- a/public/webapp/index.html +++ b/public/webapp/index.html @@ -5,18 +5,22 @@ <script src="button.js"></script> <script> const urlParams = new URLSearchParams(window.location.search); - const fileID = urlParams.get('id'); + let path = urlParams.get('path'); + if(path === null) { + const fileID = urlParams.get('id'); // legacy support + path = "/data/assets/" + fileID + ".svg"; + } document.addEventListener('DOMContentLoaded', async () => { const editLink = document.createElement('a'); - editLink.href = getEditLink(fileID); + editLink.href = getEditLink(path); document.body.appendChild(editLink); const htmlContainer = document.createElement('div'); - htmlContainer.innerHTML = await getSVG(fileID); + htmlContainer.innerHTML = await getSVG(path); editLink.appendChild(htmlContainer); - addButton(document, fileID); + addButton(document, path); }); </script> <link rel="stylesheet" href="index.css"> diff --git a/src/const.ts b/src/const.ts index 1acaa80..9e5c04e 100644 --- a/src/const.ts +++ b/src/const.ts @@ -1,7 +1,7 @@ export const SVG_MIME = "image/svg+xml"; export const JSON_MIME = "application/json"; -export const DATA_PATH = "/data/assets"; +export const DATA_PATH = "/data/assets/"; export const STORAGE_PATH = "/data/storage/petal/siyuan-jsdraw-plugin"; export const TOOLBAR_PATH = STORAGE_PATH + "/toolbar.json"; export const CONFIG_PATH = STORAGE_PATH + "/conf.json"; -export const EMBED_PATH = "/plugins/siyuan-jsdraw-plugin/webapp/?id="; \ No newline at end of file +export const EMBED_PATH = "/plugins/siyuan-jsdraw-plugin/webapp/?path="; \ No newline at end of file diff --git a/src/editorTab.ts b/src/editorTab.ts index bef75c7..0399dad 100644 --- a/src/editorTab.ts +++ b/src/editorTab.ts @@ -6,22 +6,22 @@ import {getFile, saveFile} from "@/file"; import {JSON_MIME, SVG_MIME, TOOLBAR_PATH} from "@/const"; import {idToPath} from "@/helper"; -export function openEditorTab(p: Plugin, fileID: string) { +export function openEditorTab(p: Plugin, path: string) { openTab({ app: p.app, custom: { title: 'Drawing', icon: 'iconDraw', id: "siyuan-jsdraw-pluginwhiteboard", - data: { id: fileID } + data: { path: path } } }); } -async function saveCallback(editor: Editor, fileID: string, saveButton: BaseWidget) { +async function saveCallback(editor: Editor, path: string, saveButton: BaseWidget) { const svgElem = editor.toSVG(); try { - saveFile(idToPath(fileID), SVG_MIME, svgElem.outerHTML); + saveFile(path, SVG_MIME, svgElem.outerHTML); saveButton.setDisabled(true); setTimeout(() => { // @todo improve save button feedback saveButton.setDisabled(false); @@ -36,10 +36,14 @@ async function saveCallback(editor: Editor, fileID: string, saveButton: BaseWidg export function createEditor(i: ITabModel) { - const fileID = i.data.id; - if(fileID == null) { - alert("File ID missing - couldn't open file.") - return; + let path = i.data.path; + if(path == null) { + const fileID = i.data.id; // legacy compatibility + if (fileID == null) { + alert("File ID and path missing - couldn't open file.") + return; + } + path = idToPath(fileID); } const editor = new Editor(i.element, { @@ -55,14 +59,14 @@ export function createEditor(i: ITabModel) { } }); // restore drawing - getFile(idToPath(fileID)).then(svg => { + getFile(path).then(svg => { if(svg != null) { editor.loadFromSVG(svg); } }); // save logic - const saveButton = toolbar.addSaveButton(() => saveCallback(editor, fileID, saveButton)); + const saveButton = toolbar.addSaveButton(() => saveCallback(editor, path, saveButton)); // save toolbar config on tool change (toolbar state is not saved in SVGs!) editor.notifier.on(EditorEventType.ToolUpdated, () => { diff --git a/src/helper.ts b/src/helper.ts index 0492b07..7e132ac 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -45,14 +45,14 @@ export function generateSiyuanId() { } export function idToPath(id: string) { - return DATA_PATH + '/' + id + '.svg'; + return DATA_PATH + id + '.svg'; } // [Edit](siyuan://plugins/siyuan-jsdraw-pluginwhiteboard/?icon=iconDraw&title=Drawing&data={"id":"${id}"}) //  -export function getPreviewHTML(id: string): string { +export function getPreviewHTML(path: string): string { return ` - <iframe src="${EMBED_PATH + id}&antiCache=0"></iframe> + <iframe src="${EMBED_PATH + path}&antiCache=0"></iframe> ` } @@ -60,12 +60,7 @@ export function getPreviewHTML(id: string): string { export function findImgSrc(element: HTMLElement): string | null { // Base case: if current element is an image if (element.tagName === 'IMG') { - const fullSrc = (element as HTMLImageElement).src; - // Extract the path after host:port using URL API - const url = new URL(fullSrc); - return url.pathname.startsWith('/assets/') - ? url.pathname.substring(1) // Remove leading slash - : null; + return (element as HTMLImageElement).src; } // Recursively check children @@ -79,12 +74,15 @@ export function findImgSrc(element: HTMLElement): string | null { return null; } -export function extractFileID(imgSrc: string | null): string | null { +export function imgSrcToAbsolutePath(imgSrc: string | null): string | null { if (!imgSrc) return null; - const [pathPart] = imgSrc.split('?'); - // Match pattern: assets/{fileID}.svg - const match = pathPart.match(/^assets\/([^\/]+)\.svg$/i); + const url = new URL(imgSrc); + imgSrc = decodeURIComponent(url.pathname); + + if(imgSrc.startsWith('/assets/')) { + return "/data" + imgSrc; + } + return null - return match?.[1] || null; } diff --git a/src/index.ts b/src/index.ts index 1ea3b47..851d6e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,14 @@ import {Plugin, Protyle} from 'siyuan'; -import {getPreviewHTML, loadIcons, getMenuHTML, generateSiyuanId, findImgSrc, extractFileID} from "@/helper"; +import { + getPreviewHTML, + loadIcons, + getMenuHTML, + generateSiyuanId, + findImgSrc, + imgSrcToAbsolutePath +} from "@/helper"; import {createEditor, openEditorTab} from "@/editorTab"; +import {DATA_PATH} from "@/const"; export default class DrawJSPlugin extends Plugin { onload() { @@ -18,23 +26,22 @@ export default class DrawJSPlugin extends Plugin { filter: ["Insert Drawing", "Add drawing", "whiteboard", "freehand", "graphics", "jsdraw"], html: getMenuHTML("iconDraw", this.i18n.insertDrawing), callback: (protyle: Protyle) => { - const uid = generateSiyuanId(); - protyle.insert(getPreviewHTML(uid), true, false); - openEditorTab(this, uid); + const path = DATA_PATH + generateSiyuanId() + ".svg"; + protyle.insert(getPreviewHTML(path), true, false); + openEditorTab(this, path); } }]; this.eventBus.on("open-menu-image", (e: any) => { - const fileID = extractFileID(findImgSrc(e.detail.element)); - if(fileID === null) { + const path = imgSrcToAbsolutePath(findImgSrc(e.detail.element)); + if(path === null) { return; } - console.log("got ID" + fileID); e.detail.menu.addItem({ icon: "iconDraw", label: "Edit with js-draw", click: () => { - openEditorTab(this, fileID); + openEditorTab(this, path); } }) }) From 5e51589ffa80dd102834b140ca181d18ee1d701d Mon Sep 17 00:00:00 2001 From: MassiveBox <box@massivebox.net> Date: Thu, 3 Apr 2025 00:12:36 +0200 Subject: [PATCH 04/27] Internal file paths are now Markdown image paths In the last commit, file paths were implemented to be the full path for the API, however that is not necessary. Before this change: /data/assets/filename.svg After: assets/filename.svg The new method is what SiYuan uses for Markdown images, like  --- public/webapp/button.js | 2 +- public/webapp/draw.js | 2 +- public/webapp/index.html | 2 +- src/const.ts | 5 +++-- src/editorTab.ts | 6 +++--- src/helper.ts | 9 ++++++-- src/index.ts | 8 +++---- src/protyle.ts | 46 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 66 insertions(+), 14 deletions(-) create mode 100644 src/protyle.ts diff --git a/public/webapp/button.js b/public/webapp/button.js index 5aa902d..8be8fd4 100644 --- a/public/webapp/button.js +++ b/public/webapp/button.js @@ -2,7 +2,7 @@ function copyEditLink(path) { navigator.clipboard.writeText(getEditLink(path)); } function copyImageLink(path) { - navigator.clipboard.writeText(`})`); + navigator.clipboard.writeText(``); } function refreshPage() { diff --git a/public/webapp/draw.js b/public/webapp/draw.js index baaf005..187dab2 100644 --- a/public/webapp/draw.js +++ b/public/webapp/draw.js @@ -29,7 +29,7 @@ async function getFile(path) { async function getSVG(path) { - const resp = await getFile(path); + const resp = await getFile("/data/" + path); if(resp == null) { return FALLBACK; } diff --git a/public/webapp/index.html b/public/webapp/index.html index c40fb79..cb4343c 100644 --- a/public/webapp/index.html +++ b/public/webapp/index.html @@ -8,7 +8,7 @@ let path = urlParams.get('path'); if(path === null) { const fileID = urlParams.get('id'); // legacy support - path = "/data/assets/" + fileID + ".svg"; + path = "assets/" + fileID + ".svg"; } document.addEventListener('DOMContentLoaded', async () => { diff --git a/src/const.ts b/src/const.ts index 9e5c04e..437af37 100644 --- a/src/const.ts +++ b/src/const.ts @@ -1,7 +1,8 @@ export const SVG_MIME = "image/svg+xml"; export const JSON_MIME = "application/json"; -export const DATA_PATH = "/data/assets/"; -export const STORAGE_PATH = "/data/storage/petal/siyuan-jsdraw-plugin"; +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 EMBED_PATH = "/plugins/siyuan-jsdraw-plugin/webapp/?path="; \ No newline at end of file diff --git a/src/editorTab.ts b/src/editorTab.ts index 0399dad..97f2cbc 100644 --- a/src/editorTab.ts +++ b/src/editorTab.ts @@ -3,7 +3,7 @@ import Editor, {BaseWidget, EditorEventType} from "js-draw"; import { MaterialIconProvider } from '@js-draw/material-icons'; import 'js-draw/styles'; import {getFile, saveFile} from "@/file"; -import {JSON_MIME, SVG_MIME, TOOLBAR_PATH} from "@/const"; +import {DATA_PATH, JSON_MIME, SVG_MIME, TOOLBAR_PATH} from "@/const"; import {idToPath} from "@/helper"; export function openEditorTab(p: Plugin, path: string) { @@ -21,7 +21,7 @@ export function openEditorTab(p: Plugin, path: string) { async function saveCallback(editor: Editor, path: string, saveButton: BaseWidget) { const svgElem = editor.toSVG(); try { - saveFile(path, SVG_MIME, svgElem.outerHTML); + saveFile(DATA_PATH + path, SVG_MIME, svgElem.outerHTML); saveButton.setDisabled(true); setTimeout(() => { // @todo improve save button feedback saveButton.setDisabled(false); @@ -59,7 +59,7 @@ export function createEditor(i: ITabModel) { } }); // restore drawing - getFile(path).then(svg => { + getFile(DATA_PATH + path).then(svg => { if(svg != null) { editor.loadFromSVG(svg); } diff --git a/src/helper.ts b/src/helper.ts index 7e132ac..d956158 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -74,15 +74,20 @@ export function findImgSrc(element: HTMLElement): string | null { return null; } -export function imgSrcToAbsolutePath(imgSrc: string | null): string | null { +export function imgSrcToPath(imgSrc: string | null): string | null { if (!imgSrc) return null; const url = new URL(imgSrc); imgSrc = decodeURIComponent(url.pathname); if(imgSrc.startsWith('/assets/')) { - return "/data" + imgSrc; + return imgSrc.substring(1); } return null } + +// Helper to safely escape regex special characters +export function escapeRegExp(string: string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 851d6e5..b5d4e90 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,10 +5,10 @@ import { getMenuHTML, generateSiyuanId, findImgSrc, - imgSrcToAbsolutePath + imgSrcToPath } from "@/helper"; import {createEditor, openEditorTab} from "@/editorTab"; -import {DATA_PATH} from "@/const"; +import {ASSETS_PATH} from "@/const"; export default class DrawJSPlugin extends Plugin { onload() { @@ -26,14 +26,14 @@ export default class DrawJSPlugin extends Plugin { filter: ["Insert Drawing", "Add drawing", "whiteboard", "freehand", "graphics", "jsdraw"], html: getMenuHTML("iconDraw", this.i18n.insertDrawing), callback: (protyle: Protyle) => { - const path = DATA_PATH + generateSiyuanId() + ".svg"; + const path = ASSETS_PATH + generateSiyuanId() + ".svg"; protyle.insert(getPreviewHTML(path), true, false); openEditorTab(this, path); } }]; this.eventBus.on("open-menu-image", (e: any) => { - const path = imgSrcToAbsolutePath(findImgSrc(e.detail.element)); + const path = imgSrcToPath(findImgSrc(e.detail.element)); if(path === null) { return; } diff --git a/src/protyle.ts b/src/protyle.ts new file mode 100644 index 0000000..500874a --- /dev/null +++ b/src/protyle.ts @@ -0,0 +1,46 @@ +import {getBlockByID, sql, updateBlock} from "@/api"; +import {escapeRegExp} from "@/helper"; + +export async function findImageBlocks(src: string) { + + const sqlQuery = ` + SELECT id, markdown + FROM blocks + WHERE markdown like '%${src}%' + `; + + try { + return await sql(sqlQuery); + } catch (error) { + console.error('Error searching for image blocks:', error); + return []; + } + +} +export async function replaceBlockContent( + blockId: string, + searchStr: string, + replaceStr: string +): Promise<boolean> { + try { + + const block = await getBlockByID(blockId); + if (!block) { + throw new Error('Block not found'); + } + + const originalContent = block.markdown; + const newContent = originalContent.replace(escapeRegExp(searchStr), replaceStr); + + if (newContent === originalContent) { + return false; + } + + await updateBlock('markdown', newContent, blockId); + return true; + + } catch (error) { + console.error('Failed to replace block content:', error); + return false; + } +} From e9a9961b6158c29ac42c25dba8443eb86306996b Mon Sep 17 00:00:00 2001 From: MassiveBox <box@massivebox.net> Date: Thu, 3 Apr 2025 15:51:33 +0200 Subject: [PATCH 05/27] Auto-refresh images on edit Only for Markdown images, not drawing blocks, since those will eventually be deprecated. --- src/const.ts | 3 ++- src/editorTab.ts | 2 ++ src/protyle.ts | 31 ++++++++++++++++++++++++++++--- tsconfig.json | 2 +- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/const.ts b/src/const.ts index 437af37..2f8e88a 100644 --- a/src/const.ts +++ b/src/const.ts @@ -5,4 +5,5 @@ 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 EMBED_PATH = "/plugins/siyuan-jsdraw-plugin/webapp/?path="; \ No newline at end of file +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/editorTab.ts b/src/editorTab.ts index 97f2cbc..c00b9db 100644 --- a/src/editorTab.ts +++ b/src/editorTab.ts @@ -5,6 +5,7 @@ import 'js-draw/styles'; import {getFile, saveFile} from "@/file"; import {DATA_PATH, JSON_MIME, SVG_MIME, TOOLBAR_PATH} from "@/const"; import {idToPath} from "@/helper"; +import {replaceAntiCacheID} from "@/protyle"; export function openEditorTab(p: Plugin, path: string) { openTab({ @@ -22,6 +23,7 @@ async function saveCallback(editor: Editor, path: string, saveButton: BaseWidget const svgElem = editor.toSVG(); try { saveFile(DATA_PATH + path, SVG_MIME, svgElem.outerHTML); + await replaceAntiCacheID(path); saveButton.setDisabled(true); setTimeout(() => { // @todo improve save button feedback saveButton.setDisabled(false); diff --git a/src/protyle.ts b/src/protyle.ts index 500874a..cda24f9 100644 --- a/src/protyle.ts +++ b/src/protyle.ts @@ -1,12 +1,12 @@ import {getBlockByID, sql, updateBlock} from "@/api"; -import {escapeRegExp} from "@/helper"; +import {DUMMY_HOST} from "@/const"; export async function findImageBlocks(src: string) { const sqlQuery = ` SELECT id, markdown FROM blocks - WHERE markdown like '%${src}%' + WHERE markdown like '%](${src}%' // "](" is to check it's an image src `; try { @@ -30,7 +30,7 @@ export async function replaceBlockContent( } const originalContent = block.markdown; - const newContent = originalContent.replace(escapeRegExp(searchStr), replaceStr); + const newContent = originalContent.replaceAll(searchStr, replaceStr); if (newContent === originalContent) { return false; @@ -44,3 +44,28 @@ export async function replaceBlockContent( return false; } } + +export async function replaceAntiCacheID(src: string) { + + const search = encodeURI(src); // the API uses URI-encoded + // find blocks containing that image + const blocks = await findImageBlocks(search); + + for(const block of blocks) { + + // 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) { + const url = new URL(source, DUMMY_HOST); + url.searchParams.set('antiCache', Date.now().toString()); // set or replace antiCache + const newSource = url.href.replace(DUMMY_HOST, ''); + await replaceBlockContent(block.id, source, newSource); + } + } + +} diff --git a/tsconfig.json b/tsconfig.json index 0fcc1ad..e2dedbd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "useDefineForClassFields": true, "module": "ESNext", "lib": [ - "ES2020", + "ES2021", "DOM", "DOM.Iterable" ], From e165c696643fbba38dacde48881abdd27271dc7b Mon Sep 17 00:00:00 2001 From: MassiveBox <box@massivebox.net> Date: Sat, 5 Apr 2025 00:22:38 +0200 Subject: [PATCH 06/27] Open editor in dialog on mobile SiYuan Mobile doesn't have tabs, so the editor has to be opened in a dialog. In the future, consider including a setting to open editor in dialog on desktop as well. --- src/editorTab.ts | 42 ++++++++++++++++++++++++++++-------------- src/index.ts | 6 ++---- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/editorTab.ts b/src/editorTab.ts index c00b9db..d2f89fb 100644 --- a/src/editorTab.ts +++ b/src/editorTab.ts @@ -1,13 +1,22 @@ -import {ITabModel, openTab, Plugin} from "siyuan" +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} from "@/file"; import {DATA_PATH, JSON_MIME, SVG_MIME, TOOLBAR_PATH} from "@/const"; -import {idToPath} from "@/helper"; import {replaceAntiCacheID} from "@/protyle"; +import {idToPath} from "@/helper"; export function openEditorTab(p: Plugin, path: string) { + if(getFrontend() == "mobile") { + const dialog = new Dialog({ + width: "100vw", + height: "100vh", + content: `<div id="DrawingPanel" style="width:100%; height: 100%;"></div>`, + }); + createEditor(dialog.element.querySelector("#DrawingPanel"), path); + return; + } openTab({ app: p.app, custom: { @@ -36,19 +45,9 @@ async function saveCallback(editor: Editor, path: string, saveButton: BaseWidget } -export function createEditor(i: ITabModel) { +export function createEditor(element: HTMLElement, path: string) { - let path = i.data.path; - if(path == null) { - const fileID = i.data.id; // legacy compatibility - if (fileID == null) { - alert("File ID and path missing - couldn't open file.") - return; - } - path = idToPath(fileID); - } - - const editor = new Editor(i.element, { + const editor = new Editor(element, { iconProvider: new MaterialIconProvider(), }); @@ -78,4 +77,19 @@ export function createEditor(i: ITabModel) { editor.dispatch(editor.setBackgroundStyle({ autoresize: true }), false); editor.getRootElement().style.height = '100%'; +} + +export function editorTabInit(tab: ITabModel) { + + let path = tab.data.path; + if(path == null) { + const fileID = tab.data.id; // legacy compatibility + if (fileID == null) { + alert("File ID and path missing - couldn't open file.") + return; + } + path = idToPath(fileID); + } + createEditor(tab.element, path); + } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index b5d4e90..a3d8d43 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import { findImgSrc, imgSrcToPath } from "@/helper"; -import {createEditor, openEditorTab} from "@/editorTab"; +import {editorTabInit, openEditorTab} from "@/editorTab"; import {ASSETS_PATH} from "@/const"; export default class DrawJSPlugin extends Plugin { @@ -16,9 +16,7 @@ export default class DrawJSPlugin extends Plugin { loadIcons(this); this.addTab({ 'type': "whiteboard", - init() { - createEditor(this); - } + init() { editorTabInit(this) } }); this.protyleSlash = [{ From 4555ec275fc577a85470ca4418a4f0224df98cec Mon Sep 17 00:00:00 2001 From: MassiveBox <box@massivebox.net> Date: Sat, 5 Apr 2025 19:30:31 +0200 Subject: [PATCH 07/27] Fix sync inconsistencies across devices Changed APIs to upload assets, reworked saving logic so that files are synced across devices when changed locally. --- src/editorTab.ts | 63 ++++++++++++++++++++++++++++++++---------------- src/file.ts | 17 ++++++++++++- src/helper.ts | 52 ++++++++++++++++++++++++--------------- src/index.ts | 22 ++++++++--------- src/protyle.ts | 10 +++----- 5 files changed, 104 insertions(+), 60 deletions(-) diff --git a/src/editorTab.ts b/src/editorTab.ts index d2f89fb..8ebc0a0 100644 --- a/src/editorTab.ts +++ b/src/editorTab.ts @@ -2,37 +2,51 @@ 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} from "@/file"; +import {getFile, saveFile, uploadAsset} from "@/file"; import {DATA_PATH, JSON_MIME, SVG_MIME, TOOLBAR_PATH} from "@/const"; -import {replaceAntiCacheID} from "@/protyle"; -import {idToPath} from "@/helper"; +import {replaceSyncID} from "@/protyle"; +import {IDsToAssetPath} from "@/helper"; +import {removeFile} from "@/api"; -export function openEditorTab(p: Plugin, path: string) { +export function openEditorTab(p: Plugin, fileID: string, initialSyncID: string) { if(getFrontend() == "mobile") { const dialog = new Dialog({ width: "100vw", height: "100vh", content: `<div id="DrawingPanel" style="width:100%; height: 100%;"></div>`, }); - createEditor(dialog.element.querySelector("#DrawingPanel"), path); + 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: { path: path } + data: { + fileID: fileID, + initialSyncID: initialSyncID + } } }); } -async function saveCallback(editor: Editor, path: string, saveButton: BaseWidget) { +async function saveCallback(editor: Editor, fileID: string, oldSyncID: string, saveButton: BaseWidget): Promise<string> { + const svgElem = editor.toSVG(); + let newSyncID; + try { - saveFile(DATA_PATH + path, SVG_MIME, svgElem.outerHTML); - await replaceAntiCacheID(path); + newSyncID = (await uploadAsset(fileID, SVG_MIME, svgElem.outerHTML)).syncID; + await replaceSyncID(fileID, oldSyncID, newSyncID); + await removeFile(DATA_PATH + IDsToAssetPath(fileID, oldSyncID)); saveButton.setDisabled(true); setTimeout(() => { // @todo improve save button feedback saveButton.setDisabled(false); @@ -41,11 +55,14 @@ async function saveCallback(editor: Editor, path: string, saveButton: BaseWidget 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, path: string) { +export function createEditor(element: HTMLElement, fileID: string, initialSyncID: string) { const editor = new Editor(element, { iconProvider: new MaterialIconProvider(), @@ -60,14 +77,21 @@ export function createEditor(element: HTMLElement, path: string) { } }); // restore drawing - getFile(DATA_PATH + path).then(svg => { + 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, path, saveButton)); + 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, () => { @@ -81,15 +105,12 @@ export function createEditor(element: HTMLElement, path: string) { export function editorTabInit(tab: ITabModel) { - let path = tab.data.path; - if(path == null) { - const fileID = tab.data.id; // legacy compatibility - if (fileID == null) { - alert("File ID and path missing - couldn't open file.") - return; - } - path = idToPath(fileID); + 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, path); + createEditor(tab.element, fileID, initialSyncID); } \ No newline at end of file diff --git a/src/file.ts b/src/file.ts index 5ce0d20..79e0e48 100644 --- a/src/file.ts +++ b/src/file.ts @@ -1,10 +1,25 @@ -import {getFileBlob, putFile} from "@/api"; +import {getFileBlob, putFile, upload} from "@/api"; +import {ASSETS_PATH} from "@/const"; +import {assetPathToIDs} from "@/helper"; function toFile(title: string, content: string, mimeType: string){ const blob = new Blob([content], { type: mimeType }); return new File([blob], title, { type: mimeType }); } +// upload asset to the assets folder, return fileID and syncID +export async function uploadAsset(fileID: string, mimeType: string, content: string) { + + const file = toFile(fileID + ".svg", content, mimeType); + + let r = await upload('/' + ASSETS_PATH, [file]); + if(r.errFiles) { + throw new Error("Failed to upload file"); + } + return assetPathToIDs(r.succMap[file.name]); + +} + export function saveFile(path: string, mimeType: string, content: string) { const file = toFile(path.split('/').pop(), content, mimeType); diff --git a/src/helper.ts b/src/helper.ts index d956158..ea4524d 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,5 +1,5 @@ import { Plugin } from 'siyuan'; -import {DATA_PATH, EMBED_PATH} from "@/const"; +import {ASSETS_PATH} from "@/const"; const drawIcon: string = ` <symbol id="iconDraw" viewBox="0 0 28 28"> @@ -23,7 +23,7 @@ export function getMenuHTML(icon: string, text: string): string { `; } -export function generateSiyuanId() { +export function generateTimeString() { const now = new Date(); const year = now.getFullYear().toString(); @@ -33,26 +33,45 @@ export function generateSiyuanId() { const minutes = now.getMinutes().toString().padStart(2, '0'); const seconds = now.getSeconds().toString().padStart(2, '0'); - const timestamp = `${year}${month}${day}${hours}${minutes}${seconds}`; + return `${year}${month}${day}${hours}${minutes}${seconds}`; +} + +export function generateRandomString() { const characters = 'abcdefghijklmnopqrstuvwxyz'; let random = ''; for (let i = 0; i < 7; i++) { random += characters.charAt(Math.floor(Math.random() * characters.length)); } + return random; - return `${timestamp}-${random}`; } -export function idToPath(id: string) { - return DATA_PATH + id + '.svg'; +export function IDsToAssetPath(fileID: string, syncID: string) { + return `${ASSETS_PATH}${fileID}-${syncID}.svg` +} +export function assetPathToIDs(assetPath: string): { fileID: string; syncID: string } | null { + + const filename = assetPath.split('/').pop() || ''; + if (!filename.endsWith('.svg')) return null; + + // Split into [basename, extension] and check format + const [basename] = filename.split('.'); + const parts = basename.split('-'); + + // Must contain exactly 2 hyphens separating 3 non-empty parts + if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) return null; + + return { + fileID: parts[0], + syncID: parts[1] + '-' + parts[2] + }; + } -// [Edit](siyuan://plugins/siyuan-jsdraw-pluginwhiteboard/?icon=iconDraw&title=Drawing&data={"id":"${id}"}) -//  -export function getPreviewHTML(path: string): string { +export function getMarkdownBlock(fileID: string, syncID: string): string { return ` - <iframe src="${EMBED_PATH + path}&antiCache=0"></iframe> + }) ` } @@ -74,20 +93,13 @@ export function findImgSrc(element: HTMLElement): string | null { return null; } -export function imgSrcToPath(imgSrc: string | null): string | null { +export function imgSrcToIDs(imgSrc: string | null): { fileID: string; syncID: string } | null { + if (!imgSrc) return null; const url = new URL(imgSrc); imgSrc = decodeURIComponent(url.pathname); - if(imgSrc.startsWith('/assets/')) { - return imgSrc.substring(1); - } - return null + return assetPathToIDs(imgSrc); -} - -// Helper to safely escape regex special characters -export function escapeRegExp(string: string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index a3d8d43..78f1768 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,15 @@ import {Plugin, Protyle} from 'siyuan'; import { - getPreviewHTML, + getMarkdownBlock, loadIcons, getMenuHTML, - generateSiyuanId, findImgSrc, - imgSrcToPath + imgSrcToIDs, generateTimeString, generateRandomString } from "@/helper"; import {editorTabInit, openEditorTab} from "@/editorTab"; -import {ASSETS_PATH} from "@/const"; export default class DrawJSPlugin extends Plugin { + onload() { loadIcons(this); @@ -24,22 +23,21 @@ export default class DrawJSPlugin extends Plugin { filter: ["Insert Drawing", "Add drawing", "whiteboard", "freehand", "graphics", "jsdraw"], html: getMenuHTML("iconDraw", this.i18n.insertDrawing), callback: (protyle: Protyle) => { - const path = ASSETS_PATH + generateSiyuanId() + ".svg"; - protyle.insert(getPreviewHTML(path), true, false); - openEditorTab(this, path); + const fileID = generateRandomString(); + const syncID = generateTimeString() + '-' + generateRandomString(); + protyle.insert(getMarkdownBlock(fileID, syncID), true, false); + openEditorTab(this, fileID, syncID); } }]; this.eventBus.on("open-menu-image", (e: any) => { - const path = imgSrcToPath(findImgSrc(e.detail.element)); - if(path === null) { - return; - } + const ids = imgSrcToIDs(findImgSrc(e.detail.element)); + if(ids === null) return; e.detail.menu.addItem({ icon: "iconDraw", label: "Edit with js-draw", click: () => { - openEditorTab(this, path); + openEditorTab(this, ids.fileID, ids.syncID); } }) }) diff --git a/src/protyle.ts b/src/protyle.ts index cda24f9..292221c 100644 --- a/src/protyle.ts +++ b/src/protyle.ts @@ -1,5 +1,5 @@ import {getBlockByID, sql, updateBlock} from "@/api"; -import {DUMMY_HOST} from "@/const"; +import {IDsToAssetPath} from "@/helper"; export async function findImageBlocks(src: string) { @@ -45,9 +45,9 @@ export async function replaceBlockContent( } } -export async function replaceAntiCacheID(src: string) { +export async function replaceSyncID(fileID: string, oldSyncID: string, newSyncID: string) { - const search = encodeURI(src); // the API uses URI-encoded + const search = encodeURI(IDsToAssetPath(fileID, oldSyncID)); // the API uses URI-encoded // find blocks containing that image const blocks = await findImageBlocks(search); @@ -61,9 +61,7 @@ export async function replaceAntiCacheID(src: string) { .filter(source => source.startsWith(search)) // discard other images for(const source of sources) { - const url = new URL(source, DUMMY_HOST); - url.searchParams.set('antiCache', Date.now().toString()); // set or replace antiCache - const newSource = url.href.replace(DUMMY_HOST, ''); + const newSource = IDsToAssetPath(fileID, newSyncID); await replaceBlockContent(block.id, source, newSource); } } From d8cc4f8cafa77d8b66a63f399cbb0c151978fb31 Mon Sep 17 00:00:00 2001 From: MassiveBox <box@massivebox.net> Date: Sat, 5 Apr 2025 21:58:19 +0200 Subject: [PATCH 08/27] Auto-migrate old drawing blocks on startup --- src/file.ts | 2 ++ src/index.ts | 2 ++ src/migration.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 src/migration.ts diff --git a/src/file.ts b/src/file.ts index 79e0e48..25c012d 100644 --- a/src/file.ts +++ b/src/file.ts @@ -11,8 +11,10 @@ function toFile(title: string, content: string, mimeType: string){ export async function uploadAsset(fileID: string, mimeType: string, content: string) { const file = toFile(fileID + ".svg", content, mimeType); + console.log(1, file) let r = await upload('/' + ASSETS_PATH, [file]); + console.log(2, r) if(r.errFiles) { throw new Error("Failed to upload file"); } diff --git a/src/index.ts b/src/index.ts index 78f1768..adf4b94 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { imgSrcToIDs, generateTimeString, generateRandomString } from "@/helper"; import {editorTabInit, openEditorTab} from "@/editorTab"; +import {migrate} from "@/migration"; export default class DrawJSPlugin extends Plugin { @@ -17,6 +18,7 @@ export default class DrawJSPlugin extends Plugin { 'type': "whiteboard", init() { editorTabInit(this) } }); + migrate() this.protyleSlash = [{ id: "insert-drawing", diff --git a/src/migration.ts b/src/migration.ts new file mode 100644 index 0000000..290b700 --- /dev/null +++ b/src/migration.ts @@ -0,0 +1,50 @@ +import {sql} from "@/api"; +import {getFile, uploadAsset} from "@/file"; +import {ASSETS_PATH, DATA_PATH, SVG_MIME} from "@/const"; +import {replaceBlockContent} from "@/protyle"; +import {generateRandomString, getMarkdownBlock} from "@/helper"; + +export async function migrate() { + + let blocks = await findEmbedBlocks(); + console.log(blocks); + const found = blocks.length > 0; + + 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"); + console.log("file", file) + const r = await uploadAsset(newFileID, SVG_MIME, file); + console.log("r", r); + const newMarkdown = getMarkdownBlock(r.fileID, r.syncID); + await replaceBlockContent(block.id, block.markdown, newMarkdown); + } + } + +} + +function extractID(html: string): string | null { + // Match the pattern: id= followed by characters until & or quote + const regex = /id=([^&"']+)/; + const match = html.match(regex); + return match ? match[1] : null; +} + +async function findEmbedBlocks() { + + const sqlQuery = ` + SELECT id, markdown + FROM blocks + WHERE markdown like '%src="/plugins/siyuan-jsdraw-plugin/webapp/%' + `; + + try { + return await sql(sqlQuery); + } catch (error) { + console.error('Error searching for embed blocks:', error); + return []; + } + +} \ No newline at end of file From 0bc89f4a72bd8853b1ce2f14a90fb34c14e6b94d Mon Sep 17 00:00:00 2001 From: MassiveBox <box@massivebox.net> Date: Sat, 5 Apr 2025 23:23:45 +0200 Subject: [PATCH 09/27] Prepare for release --- README.md | 14 +++++--------- package.json | 12 ++++++------ plugin.json | 2 +- public/webapp/error.html | 21 +++++++++++++++++++++ public/webapp/index.html | 2 +- src/file.ts | 2 -- src/migration.ts | 17 ++++++++++++++--- 7 files changed, 48 insertions(+), 22 deletions(-) create mode 100644 public/webapp/error.html diff --git a/README.md b/README.md index c759374..f58614c 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,16 @@ # SiYuan js-draw Plugin -This plugin allows you to embed js-draw whiteboards anywhere in your SiYuan documents. +This plugin allows you to embed js-draw whiteboards anywhere in your SiYuan documents. ## Usage instructions -- Install the plugin from the marketplace. You can find it by searching for `js-draw`. -- To edit an SVG image that is already embedded in your document: - 1. Right-click on the image, select "Plugin" > "Edit with js-draw" in the menu - 2. The editor tab will open, edit your file as you like, then click the Save button and close the tab. - 3. The image is updated, but SiYuan will still show the cached (old) image. This will be fixed in future releases, - please be patient. Until them, you can refresh the editor or change the image path. +- 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 2. The whiteboard editor will open in a new tab. Draw as you like, then click the Save button and close the tab. - 3. Click the Gear icon > Refresh to refresh the drawing block. - 4. Click the drawing block to open the editor again. +- 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 + 2. The editor tab will open, edit your file as you like, then click the Save button and close the tab. ## Planned features Check out the [Projects](https://git.massive.box/massivebox/siyuan-jsdraw-plugin/projects) tab! diff --git a/package.json b/package.json index 78f409d..f5dee54 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { - "name": "plugin-sample-vite-svelte", - "version": "0.3.6", + "name": "siyuan-jsdraw-plugin", + "version": "0.2.0", "type": "module", - "description": "This is a sample plugin based on vite and svelte for Siyuan (https://b3log.org/siyuan)", - "repository": "", - "homepage": "", - "author": "frostime", + "description": "Include a whiteboard for freehand drawing anywhere in your documents.", + "repository": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin", + "homepage": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin", + "author": "massivebox", "license": "MIT", "scripts": { "dev": "cross-env NODE_ENV=development VITE_SOURCEMAP=inline vite build --watch", diff --git a/plugin.json b/plugin.json index 0b3987f..32dcc21 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.1.1", + "version": "0.2.0", "minAppVersion": "3.0.12", "backends": [ "windows", diff --git a/public/webapp/error.html b/public/webapp/error.html new file mode 100644 index 0000000..2281841 --- /dev/null +++ b/public/webapp/error.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> + <head> + <title>Error</title> + <style> + body { + background-color: white; + color: black; + } + </style> + </head> + <body> + <p>It looks like an error occurred. You shouldn't be able to see this page.</p> + <p>No data has been deleted. Please excuse us for the inconvenience.</p> + <p> + Try reloading SiYuan, and if the error persists, open an issue at + <code>https://git.massive.box/massivebox/siyuan-jsdraw-plugin/issues</code> + or contact the developer directly via e-mail at <code>box@massive.box</code> + </p> + </body> +</html> \ No newline at end of file diff --git a/public/webapp/index.html b/public/webapp/index.html index cb4343c..9a02454 100644 --- a/public/webapp/index.html +++ b/public/webapp/index.html @@ -13,7 +13,7 @@ document.addEventListener('DOMContentLoaded', async () => { const editLink = document.createElement('a'); - editLink.href = getEditLink(path); + editLink.href = "./error.html"; document.body.appendChild(editLink); const htmlContainer = document.createElement('div'); diff --git a/src/file.ts b/src/file.ts index 25c012d..79e0e48 100644 --- a/src/file.ts +++ b/src/file.ts @@ -11,10 +11,8 @@ function toFile(title: string, content: string, mimeType: string){ export async function uploadAsset(fileID: string, mimeType: string, content: string) { const file = toFile(fileID + ".svg", content, mimeType); - console.log(1, file) let r = await upload('/' + ASSETS_PATH, [file]); - console.log(2, r) if(r.errFiles) { throw new Error("Failed to upload file"); } diff --git a/src/migration.ts b/src/migration.ts index 290b700..6ec3eac 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -3,11 +3,11 @@ import {getFile, uploadAsset} 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(); - console.log(blocks); const found = blocks.length > 0; for(const block of blocks) { @@ -15,14 +15,25 @@ export async function migrate() { if(oldFileID) { const newFileID = generateRandomString() + "-" + oldFileID; const file = await getFile(DATA_PATH + ASSETS_PATH + oldFileID + ".svg"); - console.log("file", file) const r = await uploadAsset(newFileID, SVG_MIME, file); - console.log("r", r); const newMarkdown = getMarkdownBlock(r.fileID, r.syncID); await replaceBlockContent(block.id, block.markdown, newMarkdown); } } + if(found) { + new Dialog({ + width: "90vw", + height: "90vh", + content: ` + <iframe + style="width: 100%; height: 100%; background-color: white" + src="https://notes.massive.box/YRpTbbxLiD" + /> + ` + }) + } + } function extractID(html: string): string | null { From f2801c9f1c4e48cc36e6ae29b0897332fcf34871 Mon Sep 17 00:00:00 2001 From: MassiveBox <box@massivebox.net> Date: Sun, 6 Apr 2025 12:21:25 +0200 Subject: [PATCH 10/27] Bug fix: save on no changes --- src/editorTab.ts | 14 ++++++++++++-- src/protyle.ts | 6 +++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/editorTab.ts b/src/editorTab.ts index 8ebc0a0..001320d 100644 --- a/src/editorTab.ts +++ b/src/editorTab.ts @@ -45,8 +45,18 @@ async function saveCallback(editor: Editor, fileID: string, oldSyncID: string, s try { newSyncID = (await uploadAsset(fileID, SVG_MIME, svgElem.outerHTML)).syncID; - await replaceSyncID(fileID, oldSyncID, newSyncID); - await removeFile(DATA_PATH + IDsToAssetPath(fileID, oldSyncID)); + if(newSyncID != oldSyncID) { + const changed = await replaceSyncID(fileID, oldSyncID, newSyncID); + if(!changed) { + alert( + "Error replacing old sync ID with new one! You may need to manually replace the file path." + + "\nTry saving the drawing again. This is a bug, please open an issue as soon as you can." + + "\nIf your document doesn't show the drawing, you can recover it from the SiYuan workspace directory." + ); + return oldSyncID; + } + await removeFile(DATA_PATH + IDsToAssetPath(fileID, oldSyncID)); + } saveButton.setDisabled(true); setTimeout(() => { // @todo improve save button feedback saveButton.setDisabled(false); diff --git a/src/protyle.ts b/src/protyle.ts index 292221c..b3b8905 100644 --- a/src/protyle.ts +++ b/src/protyle.ts @@ -50,6 +50,7 @@ export async function replaceSyncID(fileID: string, oldSyncID: string, newSyncID const search = encodeURI(IDsToAssetPath(fileID, oldSyncID)); // the API uses URI-encoded // find blocks containing that image const blocks = await findImageBlocks(search); + if(blocks.length === 0) return false; for(const block of blocks) { @@ -62,8 +63,11 @@ export async function replaceSyncID(fileID: string, oldSyncID: string, newSyncID for(const source of sources) { const newSource = IDsToAssetPath(fileID, newSyncID); - await replaceBlockContent(block.id, source, newSource); + const changed = await replaceBlockContent(block.id, source, newSource); + if(!changed) return false } + } + return true; } From ea9b0be8568e94f7203ef8883dc4e96644210598 Mon Sep 17 00:00:00 2001 From: MassiveBox <box@massivebox.net> Date: Sun, 6 Apr 2025 15:35:37 +0200 Subject: [PATCH 11/27] 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 <box@massivebox.net> Date: Wed, 9 Apr 2025 22:53:40 +0200 Subject: [PATCH 12/27] 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: `<div id="DrawingPanel" style="width:100%; height: 100%;"></div>`, + }); + 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: `<div id="DrawingPanel" style="width:100%; height: 100%;"></div>`, - }); - 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<string> { - - 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 <box@massivebox.net> Date: Fri, 11 Apr 2025 18:42:45 +0200 Subject: [PATCH 13/27] 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 <box@massivebox.net> Date: Tue, 15 Apr 2025 19:42:43 +0200 Subject: [PATCH 14/27] 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. + <a href='https://massive.box'>Privacy</a> + `, + 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 <box@massivebox.net> Date: Wed, 16 Apr 2025 23:56:24 +0200 Subject: [PATCH 15/27] 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. - <a href='https://massive.box'>Privacy</a> + <a href='https://s.massive.box/jsdraw-plugin-privacy'>Privacy</a> `, 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 <box@massivebox.net> Date: Thu, 17 Apr 2025 15:16:07 +0200 Subject: [PATCH 16/27] 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<string> { + + 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 <box@massivebox.net> Date: Thu, 17 Apr 2025 16:08:26 +0200 Subject: [PATCH 17/27] 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).<br /> + 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 <box@massivebox.net> Date: Thu, 17 Apr 2025 22:27:38 +0200 Subject: [PATCH 18/27] 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).<br /> 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: `<div id="DrawingPanel" style="width:100%; height: 100%;"></div>`, }); 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 <box@massivebox.net> Date: Sun, 20 Apr 2025 22:16:48 +0200 Subject: [PATCH 19/27] 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).<br />The editor will always open as a dialog on mobile." + }, + "analytics": { + "title": "Analytics", + "description": "Enable to send anonymous usage data to the developer. <a href='https://s.massive.box/jsdraw-plugin-privacy'>Privacy Policy</a>" + } + } } \ 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).<br /> - 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. - <a href='https://s.massive.box/jsdraw-plugin-privacy'>Privacy</a> - `, + 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 <box@massivebox.net> Date: Wed, 23 Apr 2025 09:52:45 +0200 Subject: [PATCH 20/27] 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<PluginEditor> { + + 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 <box@massivebox.net> Date: Thu, 1 May 2025 23:01:55 +0200 Subject: [PATCH 21/27] 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 <box@massivebox.net> Date: Mon, 5 May 2025 19:17:59 +0200 Subject: [PATCH 22/27] 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.<br /><b>This setting is only applied if \"Background Color\" is set to \"Custom\"!</b>" }, "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 <box@massivebox.net> Date: Tue, 6 May 2025 18:19:18 +0200 Subject: [PATCH 23/27] 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. <a href='https://s.massive.box/jsdraw-plugin-privacy'>Privacy Policy</a>" + }, + "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 <box@massivebox.net> Date: Tue, 6 May 2025 23:12:51 +0200 Subject: [PATCH 24/27] 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<PluginEditor> { + static async create(fileID: string, defaultEditorOptions: EditorOptions): Promise<PluginEditor> { 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 <box@massivebox.net> Date: Wed, 7 May 2025 21:16:50 +0200 Subject: [PATCH 25/27] 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;=0x<oFP)<h;3K|Lk000e1NJLTq000aC000aS1^@s62IYXH0004eX+uL$Nkc;* zaB^>EX>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+nLt<G<Pb2XO5%&@(bm;pHqD7hBaT3BDVO_0ZdLBq5Rkdt*b z1eXPyg_7QQr`Rx%v6dHw4h#@ujs@Z<Xq3oPz$HQ+3k8P+F`{k!{WX7_5*h{a=qSvx zfr(o4BmUrbwoZ0zv_2^ih0Yhx`xpZM4pb|i_kHYn)f4bN!<E(aZydqoC-^E&ZSW96 zTX1pR)U;i=+=jlVqH3!($p^^UEWDpVFa^E0(7jU6t#Qxk11yzc{s#6BU^oY%)i!%~ zwdVG3Pb0q{4n%T(1L9Tu00006VoOIv_V)k(|NkRL#AN^g010qNS#tmY3labT3lag+ z-G2N4008GnL_t(I%VT6f1K+;=|Ns9#j}L^G^&dh9IY4NA4^-6`cR=WM2O#t`MlLQ! zMn=XjTNtn#Y$pq$cRx89e&Ni8Gyng$%l5eUxc~oeCTUS-QHG#l;xKhEeK2z{TntmN ze&RO8ZHoV)E<Yd&p@SzuXx>6p8YX@~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 <box@massivebox.net> Date: Thu, 8 May 2025 22:47:09 +0200 Subject: [PATCH 26/27] 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 <box@massivebox.net> Date: Fri, 9 May 2025 22:57:42 +0200 Subject: [PATCH 27/27] 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",