From 5c261b35f2a54c4cabb66e269f29e185291c7ac5 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Tue, 1 Apr 2025 09:12:51 +0200 Subject: [PATCH 01/28] Fix bug due to empty i18n folder --- plugin.json | 2 +- public/i18n/en_US.json | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 public/i18n/en_US.json diff --git a/plugin.json b/plugin.json index 8413186..0b3987f 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.0", + "version": "0.1.1", "minAppVersion": "3.0.12", "backends": [ "windows", diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json new file mode 100644 index 0000000..b6d2382 --- /dev/null +++ b/public/i18n/en_US.json @@ -0,0 +1,3 @@ +{ + "insertDrawing": "Insert Drawing" +} \ No newline at end of file From 8d1438de331a78519686ed53bceb30001000c8c7 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Tue, 1 Apr 2025 23:03:22 +0200 Subject: [PATCH 02/28] 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(`![Drawing](assets/${fileID}.svg)`); +} function refreshPage() { window.location.reload(); @@ -20,7 +23,7 @@ function addButton(document, fileID) { popupMenu.innerHTML = ` - + `; 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 ` ` -} \ 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 Date: Tue, 1 Apr 2025 23:24:17 +0200 Subject: [PATCH 03/28] 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 Date: Wed, 2 Apr 2025 20:15:48 +0200 Subject: [PATCH 04/28] 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(`![Drawing](assets/${fileID}.svg)`); +function copyImageLink(path) { + navigator.clipboard.writeText(`![Drawing](${path.replace("/data/", "")})`); } 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 = ` - - + + `; 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 @@ 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}"}) // ![Drawing](assets/${id}.svg) -export function getPreviewHTML(id: string): string { +export function getPreviewHTML(path: string): string { return ` - + ` } @@ -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 Date: Thu, 3 Apr 2025 00:12:36 +0200 Subject: [PATCH 05/28] 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 ![Drawing](assets/filename.svg) --- 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(`![Drawing](${path.replace("/data/", "")})`); + navigator.clipboard.writeText(`![Drawing](${path})`); } 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 { + 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 Date: Thu, 3 Apr 2025 15:51:33 +0200 Subject: [PATCH 06/28] 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 Date: Sat, 5 Apr 2025 00:22:38 +0200 Subject: [PATCH 07/28] 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: `
`, + }); + 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 Date: Sat, 5 Apr 2025 19:30:31 +0200 Subject: [PATCH 08/28] 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: `
`, }); - 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 { + 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 = ` @@ -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}"}) -// ![Drawing](assets/${id}.svg) -export function getPreviewHTML(path: string): string { +export function getMarkdownBlock(fileID: string, syncID: string): string { return ` - + ![Drawing](${IDsToAssetPath(fileID, syncID)}) ` } @@ -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 Date: Sat, 5 Apr 2025 21:58:19 +0200 Subject: [PATCH 09/28] 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 Date: Sat, 5 Apr 2025 23:23:45 +0200 Subject: [PATCH 10/28] 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 @@ + + + + Error + + + +

It looks like an error occurred. You shouldn't be able to see this page.

+

No data has been deleted. Please excuse us for the inconvenience.

+

+ Try reloading SiYuan, and if the error persists, open an issue at + https://git.massive.box/massivebox/siyuan-jsdraw-plugin/issues + or contact the developer directly via e-mail at box@massive.box +

+ + \ 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: ` +