diff --git a/.forgejo/workflows/build.yml b/.github/workflows/release.yml
similarity index 60%
rename from .forgejo/workflows/build.yml
rename to .github/workflows/release.yml
index 4bfcc57..49834e5 100644
--- a/.forgejo/workflows/build.yml
+++ b/.github/workflows/release.yml
@@ -1,9 +1,7 @@
-name: Build on Push and create Release on Tag
+name: Create Release on Tag Push
 
 on:
   push:
-    branches:
-      - main
     tags:
       - "v*"
 
@@ -22,7 +20,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
@@ -30,12 +28,6 @@ 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
@@ -60,22 +52,11 @@ jobs:
       - name: Build for production
         run: pnpm build
 
-      # Move file
-      - name: Move file
-        run: mkdir built; mv package.zip built/package.zip
-
-      # Upload artifacts
-      - name: Upload artifacts
-        uses: actions/upload-artifact@v3
+      - name: Release
+        uses: ncipollo/release-action@v1
         with:
-          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 }}
+          allowUpdates: true
+          artifactErrorsFailBuild: true
+          artifacts: "package.zip"
+          token: ${{ secrets.GITHUB_TOKEN }}
+          prerelease: false
diff --git a/README.md b/README.md
index f58614c..73c10ff 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,29 @@
 
 # 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 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.
-- 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.
+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.
 
 ## Planned features
-Check out the [Projects](https://git.massive.box/massivebox/siyuan-jsdraw-plugin/projects) tab!
+- [ ] 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!
 
 ## 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!
 
diff --git a/package.json b/package.json
index 76504eb..78f409d 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,11 @@
 {
-  "name": "siyuan-jsdraw-plugin",
-  "version": "0.4.0",
+  "name": "plugin-sample-vite-svelte",
+  "version": "0.3.6",
   "type": "module",
-  "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",
+  "description": "This is a sample plugin based on vite and svelte for Siyuan (https://b3log.org/siyuan)",
+  "repository": "",
+  "homepage": "",
+  "author": "frostime",
   "license": "MIT",
   "scripts": {
     "dev": "cross-env NODE_ENV=development VITE_SOURCEMAP=inline vite build --watch",
@@ -36,7 +36,6 @@
   },
   "dependencies": {
     "@js-draw/material-icons": "^1.29.0",
-    "js-draw": "^1.29.0",
-    "ts-serializable": "^4.2.0"
+    "js-draw": "^1.29.0"
   }
 }
diff --git a/plugin.json b/plugin.json
index 8acdb80..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.4.0",
+  "version": "0.1.1",
   "minAppVersion": "3.0.12",
   "backends": [
     "windows",
@@ -31,7 +31,7 @@
   },
   "funding": {
     "custom": [
-      "https://s.massive.box/jsdraw-plugin-donate"
+      ""
     ]
   },
   "keywords": [
diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json
index fbeb088..b6d2382 100644
--- a/public/i18n/en_US.json
+++ b/public/i18n/en_US.json
@@ -1,44 +1,3 @@
 {
-  "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",
-    "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": "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",
-      "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>"
-    },
-    "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."
-    }
-  }
+  "insertDrawing": "Insert Drawing"
 }
\ No newline at end of file
diff --git a/public/webapp/button.js b/public/webapp/button.js
index 8be8fd4..610799d 100644
--- a/public/webapp/button.js
+++ b/public/webapp/button.js
@@ -1,15 +1,12 @@
-function copyEditLink(path) {
-    navigator.clipboard.writeText(getEditLink(path));
-}
-function copyImageLink(path) {
-    navigator.clipboard.writeText(`![Drawing](${path})`);
+function copyEditLink(fileID) {
+    navigator.clipboard.writeText(getEditLink(fileID));
 }
 
 function refreshPage() {
     window.location.reload();
 }
 
-function addButton(document, path) {
+function addButton(document, fileID) {
 
     // Add floating button
     const floatingButton = document.createElement('button');
@@ -22,8 +19,8 @@ function addButton(document, path) {
     popupMenu.id = 'popupMenu';
     popupMenu.innerHTML = `
                         <button onclick="refreshPage()">Refresh</button>
-                        <button onclick="copyEditLink('${path}')">Copy Direct Edit Link</button>
-                        <button onclick="copyImageLink('${path}')">Copy Image Link</button>
+                        <button onclick="copyEditLink('${fileID}')">Copy Direct Edit Link</button>
+                        
                     `;
     document.body.appendChild(popupMenu);
 
@@ -34,7 +31,6 @@ function addButton(document, path) {
 
     document.body.addEventListener('mouseleave', () => {
         floatingButton.style.display = 'none';
-        popupMenu.style.display = 'none';
     });
 
     // Toggle popup menu on button click
diff --git a/public/webapp/cursor.png b/public/webapp/cursor.png
deleted file mode 100644
index 1306cf3..0000000
Binary files a/public/webapp/cursor.png and /dev/null differ
diff --git a/public/webapp/draw.js b/public/webapp/draw.js
index 187dab2..9d9adfc 100644
--- a/public/webapp/draw.js
+++ b/public/webapp/draw.js
@@ -27,9 +27,9 @@ async function getFile(path) {
 
 }
 
-async function getSVG(path) {
+async function getSVG(fileID) {
 
-    const resp = await getFile("/data/" + path);
+    const resp = await getFile("/data/assets/" + fileID + '.svg');
     if(resp == null) {
         return FALLBACK;
     }
@@ -37,10 +37,10 @@ async function getSVG(path) {
 
 }
 
-function getEditLink(path) {
+function getEditLink(fileID) {
     const data = encodeURIComponent(
         JSON.stringify({
-            path: path,
+            id: fileID
         })
     )
     return `siyuan://plugins/siyuan-jsdraw-pluginwhiteboard/?icon=iconDraw&title=Drawing&data=${data}`;
diff --git a/public/webapp/error.html b/public/webapp/error.html
deleted file mode 100644
index 2281841..0000000
--- a/public/webapp/error.html
+++ /dev/null
@@ -1,21 +0,0 @@
-<!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 9a02454..a1fd2f6 100644
--- a/public/webapp/index.html
+++ b/public/webapp/index.html
@@ -5,22 +5,18 @@
         <script src="button.js"></script>
         <script>
             const urlParams = new URLSearchParams(window.location.search);
-            let path = urlParams.get('path');
-            if(path === null) {
-                const fileID = urlParams.get('id'); // legacy support
-                path = "assets/" + fileID + ".svg";
-            }
+            const fileID = urlParams.get('id');
 
             document.addEventListener('DOMContentLoaded', async () => {
                 const editLink = document.createElement('a');
-                editLink.href = "./error.html";
+                editLink.href = getEditLink(fileID);
                 document.body.appendChild(editLink);
 
                 const htmlContainer = document.createElement('div');
-                htmlContainer.innerHTML = await getSVG(path);
+                htmlContainer.innerHTML = await getSVG(fileID);
                 editLink.appendChild(htmlContainer);
 
-                addButton(document, path);
+                addButton(document, fileID);
             });
         </script>
         <link rel="stylesheet" href="index.css">
diff --git a/scripts/validate_tag.cjs b/scripts/validate_tag.cjs
deleted file mode 100644
index c842ffc..0000000
--- a/scripts/validate_tag.cjs
+++ /dev/null
@@ -1,24 +0,0 @@
-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);
-}
diff --git a/src/analytics.ts b/src/analytics.ts
deleted file mode 100644
index 13d37be..0000000
--- a/src/analytics.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-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
deleted file mode 100644
index 7c4bfca..0000000
--- a/src/config.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-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";
-
-export interface Options {
-    dialogOnDesktop: boolean
-    analytics: boolean
-    editorOptions: EditorOptions
-}
-export interface EditorOptions {
-    restorePosition: boolean;
-    grid: boolean
-    background: string
-}
-
-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();
-        const jsonObj = JSON.parse(this.file.getContent());
-        if(jsonObj == null) {
-            this.firstRun = true;
-        }
-        // if more than one fallback, the intermediate ones are from a legacy config file version
-        this.options = {
-            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")
-            },
-        };
-    }
-
-    async save() {
-        this.file.setContent(JSON.stringify(this.options));
-        await this.file.save();
-    }
-
-    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 {
-
-    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) {
-
-        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.editorOptions.background;
-            this.settingUtils.set('background', data.background);
-        }
-
-        this.config.setConfig({
-            dialogOnDesktop: data.dialogOnDesktop,
-            analytics: data.analytics,
-            editorOptions: {
-                grid: data.grid,
-                background: color,
-                restorePosition: data.restorePosition,
-            }
-        });
-        await this.config.save();
-
-    }
-
-    populateSettingMenu() {
-
-        this.settingUtils = new SettingUtils({
-            plugin: this.plugin,
-            name: 'optionsUI',
-            callback: async (data) => {
-                await this.configSaveCallback(data);
-            }
-        });
-
-        this.settingUtils.addItem({
-            key: "grid",
-            title: this.plugin.i18n.settings.grid.title,
-            description: this.plugin.i18n.settings.grid.description,
-            value: this.config.options.editorOptions.grid,
-            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.editorOptions.background in this.backgroundDropdownOptions ?
-                this.config.options.editorOptions.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.editorOptions.background,
-            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.editorOptions.restorePosition,
-            type: 'checkbox'
-        });
-
-        this.settingUtils.addItem({
-            key: "dialogOnDesktop",
-            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: this.plugin.i18n.settings.analytics.title,
-            description: this.plugin.i18n.settings.analytics.description,
-            value: this.config.options.analytics,
-            type: 'checkbox'
-        });
-
-    }
-
-    load() {
-        return this.settingUtils.load();
-    }
-
-}
\ No newline at end of file
diff --git a/src/const.ts b/src/const.ts
index b7d60aa..1acaa80 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -1,9 +1,7 @@
 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/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
+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
diff --git a/src/editor.ts b/src/editor.ts
deleted file mode 100644
index 1331e88..0000000
--- a/src/editor.ts
+++ /dev/null
@@ -1,251 +0,0 @@
-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,
-    Mat33,
-    Vec2,
-    Viewport
-} from "js-draw";
-import {Dialog, getFrontend, openTab, Plugin, showMessage} from "siyuan";
-import {findSyncIDInProtyle, replaceSyncID} from "@/protyle";
-import DrawJSPlugin from "@/index";
-import {EditorOptions} from "@/config";
-import 'js-draw/styles';
-import {SyncIDNotFoundError, UnchangedProtyleError} from "@/errors";
-
-export class PluginEditor {
-
-    private readonly element: HTMLElement;
-    private readonly editor: Editor;
-
-    private drawingFile: PluginAsset;
-    private toolbarFile: PluginFile;
-
-    private readonly fileID: string;
-    private syncID: string;
-
-    getElement(): HTMLElement { return this.element; }
-    getEditor(): Editor { return this.editor; }
-    getFileID(): string { return this.fileID; }
-    getSyncID(): string { return this.syncID; }
-    setSyncID(syncID: string) { this.syncID = syncID; }
-
-    private constructor(fileID: string) {
-
-        this.fileID = fileID;
-
-        this.element = document.createElement("div");
-        this.element.style.height = '100%';
-        this.editor = new Editor(this.element, {
-            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%';
-
-    }
-
-    static async create(fileID: string, defaultEditorOptions: EditorOptions): 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: EditorOptions) {
-
-        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){}
-            }
-
-        }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();
-
-        // 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();
-        if(this.toolbarFile.getContent() != null) {
-            toolbar.deserializeState(this.toolbarFile.getContent());
-        }
-
-        // save toolbar config on tool change (toolbar state is not saved in SVGs!)
-        this.editor.notifier.on(EditorEventType.ToolUpdated, () => {
-            this.toolbarFile.setContent(toolbar.serializeState());
-            this.toolbarFile.save();
-        });
-
-    }
-
-    private async saveCallback(saveButton: BaseWidget) {
-
-        const svgElem = this.editor.toSVG();
-        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();
-            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 UnchangedProtyleError();
-                await this.drawingFile.removeOld(oldSyncID);
-            }
-            saveButton.setDisabled(true);
-            setTimeout(() => { // @todo improve save button feedback
-                saveButton.setDisabled(false);
-            }, 500);
-        } catch (error) {
-            showMessage("Error saving! The current drawing has been copied to your clipboard. You may need to create a new drawing and paste it there.", 0, 'error');
-            if(error instanceof UnchangedProtyleError) {
-                showMessage("Make sure the image you're trying to edit still exists in your documents.", 0, 'error');
-            }
-            await navigator.clipboard.writeText(svgElem.outerHTML);
-            console.error(error);
-            console.log("Couldn't save SVG: ", svgElem.outerHTML)
-            return;
-        }
-
-        this.syncID = newSyncID;
-
-    }
-
-}
-
-export class EditorManager {
-
-    private editor: PluginEditor
-    setEditor(editor: PluginEditor) { this.editor = editor;}
-
-    static async create(fileID: string, p: DrawJSPlugin) {
-        let instance = new EditorManager();
-        try {
-            let editor = await PluginEditor.create(fileID, p.config.options.editorOptions);
-            instance.setEditor(editor);
-        }catch (error) {
-            EditorManager.handleCreationError(error, p);
-        }
-        return instance;
-    }
-
-    static registerTab(p: DrawJSPlugin) {
-        p.addTab({
-            'type': "whiteboard",
-            async init() {
-                const fileID = this.data.fileID;
-                if (fileID == null) {
-                    alert(p.i18n.errNoFileID);
-                    return;
-                }
-                try {
-                    const editor = await PluginEditor.create(fileID, p.config.options.editorOptions);
-                    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,
-            custom: {
-                title: p.i18n.drawing,
-                icon: 'iconDraw',
-                id: "siyuan-jsdraw-pluginwhiteboard",
-                data: {
-                    fileID: this.editor.getFileID(),
-                }
-            }
-        });
-    }
-
-    toDialog() {
-        const dialog = new Dialog({
-            width: "100vw",
-            height: getFrontend() == "mobile" ? "100vh" : "90vh",
-            content: `<div id="DrawingPanel" style="width:100%; height: 100%;"></div>`,
-        });
-        dialog.element.querySelector("#DrawingPanel").appendChild(this.editor.getElement());
-    }
-
-    open(p: DrawJSPlugin) {
-        if(getFrontend() != "mobile" && !p.config.options.dialogOnDesktop) {
-            this.toTab(p);
-        } else {
-            this.toDialog();
-        }
-    }
-
-}
\ No newline at end of file
diff --git a/src/editorTab.ts b/src/editorTab.ts
new file mode 100644
index 0000000..bef75c7
--- /dev/null
+++ b/src/editorTab.ts
@@ -0,0 +1,75 @@
+import {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 {JSON_MIME, SVG_MIME, TOOLBAR_PATH} from "@/const";
+import {idToPath} from "@/helper";
+
+export function openEditorTab(p: Plugin, fileID: string) {
+    openTab({
+        app: p.app,
+        custom: {
+            title: 'Drawing',
+            icon: 'iconDraw',
+            id: "siyuan-jsdraw-pluginwhiteboard",
+            data: { id: fileID }
+        }
+    });
+}
+
+async function saveCallback(editor: Editor, fileID: string, saveButton: BaseWidget) {
+    const svgElem = editor.toSVG();
+    try {
+        saveFile(idToPath(fileID), SVG_MIME, svgElem.outerHTML);
+        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)
+    }
+
+}
+
+export function createEditor(i: ITabModel) {
+
+    const fileID = i.data.id;
+    if(fileID == null) {
+        alert("File ID missing - couldn't open file.")
+        return;
+    }
+
+    const editor = new Editor(i.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(idToPath(fileID)).then(svg => {
+        if(svg != null) {
+            editor.loadFromSVG(svg);
+        }
+    });
+
+    // save logic
+    const saveButton = toolbar.addSaveButton(() => saveCallback(editor, fileID, saveButton));
+
+    // 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%';
+
+}
\ No newline at end of file
diff --git a/src/errors.ts b/src/errors.ts
deleted file mode 100644
index 914bd9c..0000000
--- a/src/errors.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-
-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/file.ts b/src/file.ts
index dc2a86c..5ce0d20 100644
--- a/src/file.ts
+++ b/src/file.ts
@@ -1,108 +1,37 @@
-import {getFileBlob, putFile, removeFile, upload} from "@/api";
-import {ASSETS_PATH, DATA_PATH} from "@/const";
-import {assetPathToIDs, IDsToAssetName} from "@/helper";
+import {getFileBlob, putFile} from "@/api";
 
-abstract class PluginFileBase {
+function toFile(title: string, content: string, mimeType: string){
+    const blob = new Blob([content], { type: mimeType });
+    return new File([blob], title, { type: mimeType });
+}
 
-    protected content: string | null;
+export function saveFile(path: string, mimeType: string, content: string) {
 
-    protected fileName: string;
-    protected folderPath: string;
-    protected mimeType: string;
+    const file = toFile(path.split('/').pop(), content, mimeType);
 
-    getContent() { return this.content; }
-    setContent(content: string) { this.content = content; }
-    setFileName(fileName: string) { this.fileName = fileName; }
-
-    private setFolderPath(folderPath: string) {
-        if(folderPath.startsWith('/') && folderPath.endsWith('/')) {
-            this.folderPath = folderPath;
-        }else{
-            throw new Error("folderPath must start and end with /");
-        }
-    }
-
-    // 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 });
+    try {
+        putFile(path, false, file);
+    } catch (error) {
+        console.error("Error saving file:", error);
+        throw error;
     }
 
 }
 
-export class PluginFile extends PluginFileBase {
+export async function getFile(path: string) {
 
-    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;
+    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;
         }
-    }
+    }catch {}
+
+    // js-draw expects a string!
+    return jsonText;
 
 }
-
-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 7041ba5..18386c7 100644
--- a/src/helper.ts
+++ b/src/helper.ts
@@ -1,5 +1,5 @@
 import { Plugin } from 'siyuan';
-import {ASSETS_PATH} from "@/const";
+import {DATA_PATH, EMBED_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 generateTimeString() {
+export function generateSiyuanId() {
     const now = new Date();
 
     const year = now.getFullYear().toString();
@@ -33,84 +33,25 @@ export function generateTimeString() {
     const minutes = now.getMinutes().toString().padStart(2, '0');
     const seconds = now.getSeconds().toString().padStart(2, '0');
 
-    return `${year}${month}${day}${hours}${minutes}${seconds}`;
-}
-
-export function generateRandomString() {
+    const timestamp = `${year}${month}${day}${hours}${minutes}${seconds}`;
 
     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 IDsToAssetName(fileID: string, syncID: string) {
-    return `${fileID}-${syncID}.svg`;
-}
-export function IDsToAssetPath(fileID: string, syncID: string) {
-    return `${ASSETS_PATH}${IDsToAssetName(fileID, syncID)}`
-}
-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]
-    };
-
+export function idToPath(id: string) {
+    return DATA_PATH + '/' + id + '.svg';
 }
 
-export function getMarkdownBlock(fileID: string, syncID: string): string {
+//    [Edit](siyuan://plugins/siyuan-jsdraw-pluginwhiteboard/?icon=iconDraw&title=Drawing&data={"id":"${id}"})
+//     ![Drawing](assets/${id}.svg)
+export function getPreviewHTML(id: string): string {
     return `
-    ![Drawing](${IDsToAssetPath(fileID, syncID)})
+    <iframe src="${EMBED_PATH + id}&antiCache=0"></iframe>
     `
-}
-
-// 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') {
-        return (element as HTMLImageElement).src;
-    }
-
-    // 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 imgSrcToIDs(imgSrc: string | null): { fileID: string; syncID: string } | null {
-
-    if (!imgSrc) return null;
-
-    const url = new URL(imgSrc);
-    imgSrc = decodeURIComponent(url.pathname);
-
-    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
diff --git a/src/index.ts b/src/index.ts
index 9d02d6d..e19ecb2 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,82 +1,44 @@
 import {Plugin, Protyle} from 'siyuan';
-import {
-    getMarkdownBlock,
-    loadIcons,
-    getMenuHTML,
-    findImgSrc,
-    imgSrcToIDs, generateTimeString, generateRandomString
-} from "@/helper";
-import {migrate} from "@/migration";
-import {EditorManager} from "@/editor";
-import {PluginConfig, PluginConfigViewer} from "@/config";
-import {Analytics} from "@/analytics";
+import {getPreviewHTML, loadIcons, getMenuHTML, generateSiyuanId} from "@/helper";
+import {createEditor, openEditorTab} from "@/editorTab";
 
 export default class DrawJSPlugin extends Plugin {
-
-    config: PluginConfig;
-    analytics: Analytics;
-
-    async onload() {
+    onload() {
 
         loadIcons(this);
-        EditorManager.registerTab(this);
-        migrate()
-
-        await this.startConfig();
-        await this.startAnalytics();
+        //const id = Math.random().toString(36).substring(7);
+        this.addTab({
+            'type': "whiteboard",
+            init() {
+                createEditor(this);
+            }
+        });
 
         this.protyleSlash = [{
             id: "insert-drawing",
             filter: ["Insert Drawing", "Add drawing", "whiteboard", "freehand", "graphics", "jsdraw"],
             html: getMenuHTML("iconDraw", this.i18n.insertDrawing),
-            callback: async (protyle: Protyle) => {
-                void this.analytics.sendEvent('create');
-                const fileID = generateRandomString();
-                const syncID = generateTimeString() + '-' + generateRandomString();
-                protyle.insert(getMarkdownBlock(fileID, syncID), true, false);
-                (await EditorManager.create(fileID, this)).open(this);
+            callback: (protyle: Protyle) => {
+                const uid = generateSiyuanId();
+                protyle.insert(getPreviewHTML(uid), true, false);
+                openEditorTab(this, uid);
             }
         }];
 
-        this.eventBus.on("open-menu-image", (e: any) => {
-            const ids = imgSrcToIDs(findImgSrc(e.detail.element));
-            if (ids === null) return;
-            e.detail.menu.addItem({
-                icon: "iconDraw",
-                label: this.i18n.editDrawing,
-                click: async () => {
-                    void this.analytics.sendEvent('edit');
-                    (await EditorManager.create(ids.fileID, this)).open(this);
-                }
-            })
-        })
+    }
 
+    onLayoutReady() {
+        // This function is automatically called when the layout is loaded.
     }
 
     onunload() {
-        void this.analytics.sendEvent("unload");
+        // This function is automatically called when the plugin is disabled.
     }
 
     uninstall() {
-        void this.analytics.sendEvent("uninstall");
+        // This function is automatically called when the plugin is uninstalled.
     }
 
-    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
diff --git a/src/migration.ts b/src/migration.ts
deleted file mode 100644
index 2829aad..0000000
--- a/src/migration.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import {sql} from "@/api";
-import {PluginAsset, PluginFile} from "@/file";
-import {ASSETS_PATH, DATA_PATH, SVG_MIME} from "@/const";
-import {replaceBlockContent} from "@/protyle";
-import {generateRandomString, getMarkdownBlock} from "@/helper";
-import {Dialog} from "siyuan";
-
-export async function migrate() {
-
-    let blocks = await findEmbedBlocks();
-    const found = blocks.length > 0;
-
-    for(const block of blocks) {
-        const oldFileID = extractID(block.markdown);
-        if(oldFileID) {
-            const oldFile = new PluginFile(DATA_PATH + ASSETS_PATH, oldFileID + '.svg', SVG_MIME);
-            await oldFile.loadFromSiYuanFS();
-            const newFile = new PluginAsset(generateRandomString(), oldFileID, SVG_MIME);
-            newFile.setContent(oldFile.getContent());
-            await newFile.save();
-            const newMarkdown = getMarkdownBlock(newFile.getFileID(), newFile.getSyncID());
-            if(await replaceBlockContent(block.id, block.markdown, newMarkdown)) {
-                await oldFile.remove();
-            }
-        }
-    }
-
-    if(found) {
-        new Dialog({
-            width: "90vw",
-            height: "90vh",
-            content: `
-                <iframe 
-                    style="width: 100%; height: 100%; background-color: white"
-                    src="https://notes.massive.box/YRpTbbxLiD" 
-                />
-            `
-        })
-    }
-
-}
-
-function extractID(html: string): string | null {
-    // Match the pattern: id= followed by characters until &amp; 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
diff --git a/src/protyle.ts b/src/protyle.ts
deleted file mode 100644
index 5c3e842..0000000
--- a/src/protyle.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import {getBlockByID, sql, updateBlock} from "@/api";
-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 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."
-                );
-            }
-        }
-    }
-
-    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) {
-
-    const sqlQuery = `
-        SELECT id, markdown 
-        FROM blocks 
-        WHERE markdown like '%](${src}%' // "](" is to check it's an image 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.replaceAll(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;
-    }
-}
-
-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
-    // find blocks containing that image
-    const blocks = await findImageBlocks(search);
-    if(blocks.length === 0) return false;
-
-    for(const block of blocks) {
-
-        // get all the image sources, with parameters
-        const markdown = block.markdown;
-
-        for(const source of extractImageSourcesFromMarkdown(markdown, search)) {
-            const newSource = IDsToAssetPath(fileID, newSyncID);
-            const changed = await replaceBlockContent(block.id, source, newSource);
-            if(!changed) return false
-        }
-
-    }
-    return true;
-
-}
diff --git a/tsconfig.json b/tsconfig.json
index e2dedbd..0fcc1ad 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -4,7 +4,7 @@
         "useDefineForClassFields": true,
         "module": "ESNext",
         "lib": [
-            "ES2021",
+            "ES2020",
             "DOM",
             "DOM.Iterable"
         ],
diff --git a/vite.config.ts.timestamp-1743541342564-d66840ad6dd8b.mjs b/vite.config.ts.timestamp-1743541342564-d66840ad6dd8b.mjs
deleted file mode 100644
index f2c6618..0000000
--- a/vite.config.ts.timestamp-1743541342564-d66840ad6dd8b.mjs
+++ /dev/null
@@ -1,185 +0,0 @@
-// 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==