Compare commits

..

No commits in common. "main" and "v0.3.0" have entirely different histories.
main ... v0.3.0

15 changed files with 213 additions and 444 deletions

View file

@ -1,9 +1,7 @@
name: Build on Push and create Release on Tag name: Create Release on Tag Push
on: on:
push: push:
branches:
- main
tags: tags:
- "v*" - "v*"
@ -22,7 +20,7 @@ jobs:
node-version: 20 node-version: 20
registry-url: "https://registry.npmjs.org" registry-url: "https://registry.npmjs.org"
# Install pnpm # Install pnpm
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
id: pnpm-install id: pnpm-install
@ -30,12 +28,6 @@ jobs:
version: 8 version: 8
run_install: false 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 # Get pnpm store directory
- name: Get pnpm store directory - name: Get pnpm store directory
id: pnpm-cache id: pnpm-cache
@ -60,22 +52,11 @@ jobs:
- name: Build for production - name: Build for production
run: pnpm build run: pnpm build
# Move file - name: Release
- name: Move file uses: ncipollo/release-action@v1
run: mkdir built; mv package.zip built/package.zip
# Upload artifacts
- name: Upload artifacts
uses: actions/upload-artifact@v3
with: with:
path: built/package.zip allowUpdates: true
overwrite: true artifactErrorsFailBuild: true
artifacts: "package.zip"
# Create Forgejo Release token: ${{ secrets.GITHUB_TOKEN }}
- name: Create Forgejo Release prerelease: false
if: github.ref_type == 'tag'
uses: actions/forgejo-release@v1
with:
direction: upload
release-dir: built
token: ${{ secrets.FORGE_TOKEN }}

View file

@ -5,12 +5,11 @@ This plugin allows you to embed js-draw whiteboards anywhere in your SiYuan docu
## Usage instructions ## Usage instructions
- Install the plugin from the marketplace. You can find it by searching for `js-draw`. - Install the plugin from the marketplace. You can find it by searching for `js-draw`.
- To add a new whiteboard to your document: - To add a new drawing to your document:
1. Type `/Insert whiteboard` in your document, and select the correct menu entry 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. 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 whiteboard later: - To edit the image later:
1. Left-click or tap on the whiteboard to select it, then click on the Edit icon in the top bar 1. Right-click on the image (or click the three dots on mobile), select "Plugin" > "Edit with js-draw" in the menu
- Or right-click on the whiteboard (or click the three dots on mobile), select "Plugin" > "Edit whiteboard" in the menu
2. The editor tab will open, edit your file as you like, then click the Save button and close the tab. 2. The editor tab will open, edit your file as you like, then click the Save button and close the tab.
## Planned features ## Planned features

View file

@ -1,6 +1,6 @@
{ {
"name": "siyuan-jsdraw-plugin", "name": "siyuan-jsdraw-plugin",
"version": "0.4.1", "version": "0.3.0",
"type": "module", "type": "module",
"description": "Include a whiteboard for freehand drawing anywhere in your documents.", "description": "Include a whiteboard for freehand drawing anywhere in your documents.",
"repository": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin", "repository": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin",
@ -36,7 +36,6 @@
}, },
"dependencies": { "dependencies": {
"@js-draw/material-icons": "^1.29.0", "@js-draw/material-icons": "^1.29.0",
"js-draw": "^1.29.0", "js-draw": "^1.29.0"
"ts-serializable": "^4.2.0"
} }
} }

View file

@ -2,7 +2,7 @@
"name": "siyuan-jsdraw-plugin", "name": "siyuan-jsdraw-plugin",
"author": "massivebox", "author": "massivebox",
"url": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin", "url": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin",
"version": "0.4.1", "version": "0.3.0",
"minAppVersion": "3.0.12", "minAppVersion": "3.0.12",
"backends": [ "backends": [
"windows", "windows",
@ -31,7 +31,7 @@
}, },
"funding": { "funding": {
"custom": [ "custom": [
"https://s.massive.box/jsdraw-plugin-donate" ""
] ]
}, },
"keywords": [ "keywords": [

View file

@ -1,50 +1,3 @@
{ {
"insertWhiteboard": "Insert whiteboard", "insertDrawing": "Insert Drawing"
"editWhiteboard": "Edit whiteboard",
"editShortcut": "Edit selected whiteboard",
"errNoFileID": "File ID missing - couldn't open file.",
"errNotAWhiteboard": "You must select a whiteboard, not a regular image. <a href='https://s.massive.box/jsdraw-plugin-instructions'>Usage instructions</a>",
"errSyncIDNotFound": "Couldn't find SyncID in document for drawing, make sure you're trying to edit a whiteboard that is included in at least a note.",
"errCreateUnknown": "Unknown error while creating editor, please try again.",
"errInvalidBackgroundColor": "Invalid background color! Please enter an HEX color, like #000000 (black) or #FFFFFF (white). The old background color will be used.",
"errMultipleSyncIDs": "Multiple syncIDs found in documents. Remove the drawings that don't exist from your documents.\n Sync conflict copies can cause this error, so make sure to delete them.\nFile IDs (the part you can change in the Rename menu) must be unique across all documents.\n<a href='https://git.massive.box/massivebox/siyuan-jsdraw-plugin/wiki/Errors-and-Fixes#multiple-syncids-found'>Full explanation</a>",
"errUnchangedProtyle": "Make sure the image you're trying to edit still exists in your documents.",
"errSaveGeneric": "Error saving! The current drawing has been copied to your clipboard. You may need to create a new drawing and paste it there.",
"errMustSelect": "Select a whiteboard in your document by left-clicking it, then use this icon/shortcut to open the editor directly. <a href='https://s.massive.box/jsdraw-plugin-instructions'>Usage instructions</a>",
"whiteboard": "Whiteboard",
"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 whiteboards."
},
"backgroundDropdown":{
"title": "Background color",
"description": "Default background color for new whiteboards."
},
"background": {
"title": "Custom background",
"description": "Hexadecimal code of the custom background color for new whiteboards.<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 same whiteboard."
}
}
} }

Binary file not shown.

Before

(image error) Size: 606 B

View file

@ -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);
}

View file

@ -2,16 +2,16 @@ import {PluginFile} from "@/file";
import {CONFIG_FILENAME, JSON_MIME, STORAGE_PATH} from "@/const"; import {CONFIG_FILENAME, JSON_MIME, STORAGE_PATH} from "@/const";
import {Plugin} from "siyuan"; import {Plugin} from "siyuan";
import {SettingUtils} from "@/libs/setting-utils"; import {SettingUtils} from "@/libs/setting-utils";
import {getFirstDefined} from "@/helper"; import {validateColor} from "@/helper";
import {ErrorReporter, InvalidBackgroundColorError} from "@/errors";
export interface Options { type Options = {
grid: boolean
background: string
dialogOnDesktop: boolean dialogOnDesktop: boolean
analytics: boolean analytics: boolean
editorOptions: EditorOptions };
}
export interface EditorOptions { export type DefaultEditorOptions = {
restorePosition: boolean;
grid: boolean grid: boolean
background: string background: string
} }
@ -29,23 +29,30 @@ export class PluginConfig {
this.file = new PluginFile(STORAGE_PATH, CONFIG_FILENAME, JSON_MIME); this.file = new PluginFile(STORAGE_PATH, CONFIG_FILENAME, JSON_MIME);
} }
getDefaultEditorOptions(): DefaultEditorOptions {
return {
grid: this.options.grid,
background: this.options.background,
};
}
async load() { async load() {
this.firstRun = false; this.firstRun = false;
await this.file.loadFromSiYuanFS(); await this.file.loadFromSiYuanFS();
const jsonObj = JSON.parse(this.file.getContent()); this.options = JSON.parse(this.file.getContent());
if(jsonObj == null) { if(this.options == null) {
this.firstRun = true; this.loadDefaultConfig();
} }
// if more than one fallback, the intermediate ones are from a legacy config file version }
private loadDefaultConfig() {
this.options = { this.options = {
dialogOnDesktop: getFirstDefined(jsonObj?.dialogOnDesktop, false), grid: true,
analytics: getFirstDefined(jsonObj?.analytics, true), background: "#000000",
editorOptions: { dialogOnDesktop: false,
restorePosition: getFirstDefined(jsonObj?.editorOptions?.restorePosition, jsonObj?.restorePosition, true), analytics: true,
grid: getFirstDefined(jsonObj?.editorOptions?.grid, jsonObj?.grid, true),
background: getFirstDefined(jsonObj?.editorOptions?.background, jsonObj?.background, "#00000000")
},
}; };
this.firstRun = true;
} }
async save() { async save() {
@ -54,14 +61,12 @@ export class PluginConfig {
} }
setConfig(config: Options) { setConfig(config: Options) {
this.options = config; if(!validateColor(config.background)) {
} alert("Invalid background color! Please enter an HEX color, like #000000 (black) or #FFFFFF (white)");
config.background = this.options.background;
}
static validateColor(hex: string) { this.options = config;
hex = hex.replace('#', '');
return typeof hex === 'string'
&& (hex.length === 6 || hex.length === 8)
&& !isNaN(Number('0x' + hex))
} }
} }
@ -71,100 +76,62 @@ export class PluginConfigViewer {
config: PluginConfig; config: PluginConfig;
settingUtils: SettingUtils; settingUtils: SettingUtils;
plugin: Plugin; plugin: Plugin;
private readonly backgroundDropdownOptions;
constructor(config: PluginConfig, plugin: Plugin) { constructor(config: PluginConfig, plugin: Plugin) {
this.config = config; this.config = config;
this.plugin = plugin; 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(); this.populateSettingMenu();
} }
async configSaveCallback(data) {
let color = data.backgroundDropdown === "CUSTOM" ? data.background : data.backgroundDropdown;
if(!PluginConfig.validateColor(color)) {
ErrorReporter.error(new InvalidBackgroundColorError());
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() { populateSettingMenu() {
this.settingUtils = new SettingUtils({ this.settingUtils = new SettingUtils({
plugin: this.plugin, plugin: this.plugin,
name: 'optionsUI',
callback: async (data) => { callback: async (data) => {
await this.configSaveCallback(data); this.config.setConfig({
grid: data.grid,
background: data.background,
dialogOnDesktop: data.dialogOnDesktop,
analytics: data.analytics,
});
await this.config.save();
} }
}); });
this.settingUtils.addItem({ this.settingUtils.addItem({
key: "grid", key: "grid",
title: this.plugin.i18n.settings.grid.title, title: "Enable grid by default",
description: this.plugin.i18n.settings.grid.description, description: "Enable to automatically turn on the grid on new drawings.",
value: this.config.options.editorOptions.grid, value: this.config.options.grid,
type: 'checkbox' 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({ this.settingUtils.addItem({
key: "background", key: "background",
title: this.plugin.i18n.settings.background.title, title: "Default background Color",
description: this.plugin.i18n.settings.background.description, description: "Default background color of the drawing area for new drawings in hexadecimal.",
value: this.config.options.editorOptions.background, value: this.config.options.background,
type: 'textinput', type: 'textarea',
});
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({ this.settingUtils.addItem({
key: "dialogOnDesktop", key: "dialogOnDesktop",
title: this.plugin.i18n.settings.dialogOnDesktop.title, title: "Open editor as dialog on desktop",
description: this.plugin.i18n.settings.dialogOnDesktop.description, description: `
Dialog mode provides a larger drawing area, but it's not as handy to use as tabs (default).<br />
The editor will always open as a dialog on mobile.
`,
value: this.config.options.dialogOnDesktop, value: this.config.options.dialogOnDesktop,
type: 'checkbox' type: 'checkbox'
}); });
this.settingUtils.addItem({ this.settingUtils.addItem({
key: "analytics", key: "analytics",
title: this.plugin.i18n.settings.analytics.title, title: "Analytics",
description: this.plugin.i18n.settings.analytics.description, description: `
Enable to send anonymous usage data to the developer.
<a href='https://s.massive.box/jsdraw-plugin-privacy'>Privacy</a>
`,
value: this.config.options.analytics, value: this.config.options.analytics,
type: 'checkbox' type: 'checkbox'
}); });

View file

@ -1,25 +1,12 @@
import {MaterialIconProvider} from "@js-draw/material-icons"; import {MaterialIconProvider} from "@js-draw/material-icons";
import {PluginAsset, PluginFile} from "@/file"; import {PluginAsset, PluginFile} from "@/file";
import {JSON_MIME, STORAGE_PATH, SVG_MIME, TOOLBAR_FILENAME} from "@/const"; import {JSON_MIME, STORAGE_PATH, SVG_MIME, TOOLBAR_FILENAME} from "@/const";
import Editor, { import Editor, {BackgroundComponentBackgroundType, BaseWidget, Color4, EditorEventType} from "js-draw";
BackgroundComponentBackgroundType,
BaseWidget,
Color4,
EditorEventType,
Mat33,
Vec2,
Viewport
} from "js-draw";
import {Dialog, getFrontend, openTab, Plugin} from "siyuan"; import {Dialog, getFrontend, openTab, Plugin} from "siyuan";
import {findSyncIDInProtyle, replaceSyncID} from "@/protyle"; import {findSyncIDInProtyle, replaceSyncID} from "@/protyle";
import DrawJSPlugin from "@/index"; import DrawJSPlugin from "@/index";
import {EditorOptions} from "@/config"; import {DefaultEditorOptions} from "@/config";
import 'js-draw/styles'; import 'js-draw/styles';
import {
ErrorReporter,
GenericSaveError, InternationalizedError, NoFileIDError, SyncIDNotFoundError,
UnchangedProtyleError
} from "@/errors";
export class PluginEditor { export class PluginEditor {
@ -36,9 +23,8 @@ export class PluginEditor {
getEditor(): Editor { return this.editor; } getEditor(): Editor { return this.editor; }
getFileID(): string { return this.fileID; } getFileID(): string { return this.fileID; }
getSyncID(): string { return this.syncID; } getSyncID(): string { return this.syncID; }
setSyncID(syncID: string) { this.syncID = syncID; }
private constructor(fileID: string) { constructor(fileID: string, defaultEditorOptions: DefaultEditorOptions) {
this.fileID = fileID; this.fileID = fileID;
@ -48,87 +34,61 @@ export class PluginEditor {
iconProvider: new MaterialIconProvider(), iconProvider: new MaterialIconProvider(),
}); });
const styleElement = document.createElement('style'); this.genToolbar().then(() => {
styleElement.innerHTML = ` this.editor.dispatch(this.editor.setBackgroundStyle({ autoresize: true }), false);
canvas.wetInkCanvas { this.editor.getRootElement().style.height = '100%';
cursor: url('/plugins/siyuan-jsdraw-plugin/webapp/cursor.png') 3 3, none; });
}
`;
this.element.appendChild(styleElement);
this.editor.dispatch(this.editor.setBackgroundStyle({ autoresize: true }), false); findSyncIDInProtyle(this.fileID).then(async (syncID) => {
this.editor.getRootElement().style.height = '100%';
} if(syncID == null) {
alert(
static async create(fileID: string, defaultEditorOptions: EditorOptions): Promise<PluginEditor> { "Couldn't find SyncID in protyle for this file.\n" +
"Make sure the drawing you're trying to edit exists in a note.\n" +
const instance = new PluginEditor(fileID); "Close this editor tab now, and try to open the editor again."
);
await instance.genToolbar(); return;
let syncID = await findSyncIDInProtyle(fileID);
if(syncID == null) {
throw new SyncIDNotFoundError();
}
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{ this.syncID = syncID;
// it's a new drawing // restore drawing
this.editor.dispatch(this.editor.setBackgroundStyle({ this.drawingFile = new PluginAsset(this.fileID, syncID, SVG_MIME);
color: Color4.fromHex(defaultEditorOptions.background), await this.drawingFile.loadFromSiYuanFS();
type: defaultEditorOptions.grid ? BackgroundComponentBackgroundType.Grid : BackgroundComponentBackgroundType.SolidColor,
autoresize: true if(this.drawingFile.getContent() != null) {
})); await this.editor.loadFromSVG(this.drawingFile.getContent());
} }else{
// it's a new drawing
this.editor.dispatch(this.editor.setBackgroundStyle({
color: Color4.fromHex(defaultEditorOptions.background),
type: defaultEditorOptions.grid ? BackgroundComponentBackgroundType.Grid : BackgroundComponentBackgroundType.SolidColor,
autoresize: true
}));
}
}).catch((error) => {
alert("Error loading drawing: " + error);
});
} }
async genToolbar() { private async genToolbar() {
const toolbar = this.editor.addToolbar(); const toolbar = this.editor.addToolbar();
// restore toolbarFile state
this.toolbarFile = new PluginFile(STORAGE_PATH, TOOLBAR_FILENAME, JSON_MIME);
this.toolbarFile.loadFromSiYuanFS().then(() => {
if(this.toolbarFile.getContent() != null) {
toolbar.deserializeState(this.toolbarFile.getContent());
}
});
// save button // save button
const saveButton = toolbar.addSaveButton(async () => { const saveButton = toolbar.addSaveButton(async () => {
await this.saveCallback(saveButton); 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!) // save toolbar config on tool change (toolbar state is not saved in SVGs!)
this.editor.notifier.on(EditorEventType.ToolUpdated, () => { this.editor.notifier.on(EditorEventType.ToolUpdated, () => {
this.toolbarFile.setContent(toolbar.serializeState()); this.toolbarFile.setContent(toolbar.serializeState());
@ -143,17 +103,13 @@ export class PluginEditor {
let newSyncID: string; let newSyncID: string;
const oldSyncID = this.syncID; 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 { try {
this.drawingFile.setContent(svgElem.outerHTML); this.drawingFile.setContent(svgElem.outerHTML);
await this.drawingFile.save(); await this.drawingFile.save();
newSyncID = this.drawingFile.getSyncID(); newSyncID = this.drawingFile.getSyncID();
if(newSyncID != oldSyncID) { // supposed to replace protyle if(newSyncID != oldSyncID) { // supposed to replace protyle
const changed = await replaceSyncID(this.fileID, oldSyncID, newSyncID); // try to change protyle const changed = await replaceSyncID(this.fileID, oldSyncID, newSyncID); // try to change protyle
if(!changed) throw new UnchangedProtyleError(); if(!changed) throw new Error("Couldn't replace old images in protyle");
await this.drawingFile.removeOld(oldSyncID); await this.drawingFile.removeOld(oldSyncID);
} }
saveButton.setDisabled(true); saveButton.setDisabled(true);
@ -161,13 +117,9 @@ export class PluginEditor {
saveButton.setDisabled(false); saveButton.setDisabled(false);
}, 500); }, 500);
} catch (error) { } catch (error) {
if(error instanceof InternationalizedError) { alert("Error saving! The current drawing has been copied to your clipboard. You may need to create a new drawing and paste it there.");
ErrorReporter.error(error);
}else{
ErrorReporter.error(new GenericSaveError());
console.error(error);
}
await navigator.clipboard.writeText(svgElem.outerHTML); await navigator.clipboard.writeText(svgElem.outerHTML);
console.error(error);
console.log("Couldn't save SVG: ", svgElem.outerHTML) console.log("Couldn't save SVG: ", svgElem.outerHTML)
return; return;
} }
@ -181,34 +133,22 @@ export class PluginEditor {
export class EditorManager { export class EditorManager {
private editor: PluginEditor private editor: PluginEditor
setEditor(editor: PluginEditor) { this.editor = editor;}
static async create(fileID: string, p: DrawJSPlugin) { constructor(fileID: string, defaultEditorOptions: DefaultEditorOptions) {
let instance = new EditorManager(); this.editor = new PluginEditor(fileID, defaultEditorOptions);
try {
let editor = await PluginEditor.create(fileID, p.config.options.editorOptions);
instance.setEditor(editor);
}catch (error) {
ErrorReporter.error(error);
}
return instance;
} }
static registerTab(p: DrawJSPlugin) { static registerTab(p: DrawJSPlugin) {
p.addTab({ p.addTab({
'type': "whiteboard", 'type': "whiteboard",
async init() { init() {
const fileID = this.data.fileID; const fileID = this.data.fileID;
if (fileID == null) { if (fileID == null) {
ErrorReporter.error(new NoFileIDError()); alert("File ID missing - couldn't open file.")
return; return;
} }
try { const editor = new PluginEditor(fileID, p.config.getDefaultEditorOptions());
const editor = await PluginEditor.create(fileID, p.config.options.editorOptions); this.element.appendChild(editor.getElement());
this.element.appendChild(editor.getElement());
}catch (error){
ErrorReporter.error(error);
}
} }
}); });
} }
@ -217,7 +157,7 @@ export class EditorManager {
openTab({ openTab({
app: p.app, app: p.app,
custom: { custom: {
title: p.i18n.whiteboard, title: 'Drawing',
icon: 'iconDraw', icon: 'iconDraw',
id: "siyuan-jsdraw-pluginwhiteboard", id: "siyuan-jsdraw-pluginwhiteboard",
data: { data: {
@ -236,7 +176,7 @@ export class EditorManager {
dialog.element.querySelector("#DrawingPanel").appendChild(this.editor.getElement()); dialog.element.querySelector("#DrawingPanel").appendChild(this.editor.getElement());
} }
open(p: DrawJSPlugin) { async open(p: DrawJSPlugin) {
if(getFrontend() != "mobile" && !p.config.options.dialogOnDesktop) { if(getFrontend() != "mobile" && !p.config.options.dialogOnDesktop) {
this.toTab(p); this.toTab(p);
} else { } else {

View file

@ -1,80 +0,0 @@
import {showMessage} from "siyuan";
export class InternationalizedError extends Error {
readonly key: string;
constructor(key: string) {
super(key);
this.key = key;
}
}
export class ErrorReporter {
static i18n: any;
constructor(i18n: any) {
ErrorReporter.i18n = i18n;
}
static error(err: Error, timeout?: number) {
console.error(err);
let errorTxt = err.message;
if(err instanceof InternationalizedError) {
errorTxt = ErrorReporter.i18n[err.key];
}
if(!timeout) {
timeout = 0;
}
showMessage(errorTxt, timeout, 'error');
}
}
export class SyncIDNotFoundError extends InternationalizedError {
constructor() {
super('errSyncIDNotFound');
}
}
export class UnchangedProtyleError extends InternationalizedError {
constructor() {
super('errUnchangedProtyle');
}
}
export class MultipleSyncIDsError extends InternationalizedError {
constructor() {
super('errMultipleSyncIDs');
}
}
export class GenericSaveError extends InternationalizedError {
constructor() {
super('errSaveGeneric');
}
}
export class NotAWhiteboardError extends InternationalizedError {
constructor() {
super('errNotAWhiteboard');
}
}
export class InvalidBackgroundColorError extends InternationalizedError {
constructor() {
super('errInvalidBackgroundColor');
}
}
export class NoFileIDError extends InternationalizedError {
constructor() {
super('errNoFileID');
}
}
export class MustSelectError extends InternationalizedError {
constructor() {
super('errMustSelect');
}
}

View file

@ -52,7 +52,7 @@ abstract class PluginFileBase {
protected toFile(customFilename?: string): File { protected toFile(customFilename?: string): File {
let filename = customFilename || this.fileName; let filename = customFilename || this.fileName;
const blob = new Blob([this.content], { type: this.mimeType }); const blob = new Blob([this.content], { type: this.mimeType });
return new File([blob], filename, { type: this.mimeType, lastModified: Date.now() }); return new File([blob], filename, { type: this.mimeType });
} }
} }

View file

@ -107,10 +107,9 @@ export function imgSrcToIDs(imgSrc: string | null): { fileID: string; syncID: st
} }
export function getFirstDefined(...a) { export function validateColor(hex: string) {
for(let i = 0; i < a.length; i++) { hex = hex.replace('#', '');
if(a[i] !== undefined) { return typeof hex === 'string'
return a[i]; && hex.length === 6
} && !isNaN(Number('0x' + hex))
}
} }

View file

@ -6,10 +6,10 @@ import {
findImgSrc, findImgSrc,
imgSrcToIDs, generateTimeString, generateRandomString imgSrcToIDs, generateTimeString, generateRandomString
} from "@/helper"; } from "@/helper";
import {migrate} from "@/migration";
import {EditorManager} from "@/editor"; import {EditorManager} from "@/editor";
import {PluginConfig, PluginConfigViewer} from "@/config"; import {PluginConfig, PluginConfigViewer} from "@/config";
import {Analytics} from "@/analytics"; import {Analytics} from "@/analytics";
import {ErrorReporter, MustSelectError, NotAWhiteboardError} from "@/errors";
export default class DrawJSPlugin extends Plugin { export default class DrawJSPlugin extends Plugin {
@ -18,23 +18,23 @@ export default class DrawJSPlugin extends Plugin {
async onload() { async onload() {
new ErrorReporter(this.i18n);
loadIcons(this); loadIcons(this);
EditorManager.registerTab(this); EditorManager.registerTab(this);
migrate()
await this.startConfig(); await this.startConfig();
await this.startAnalytics(); await this.startAnalytics();
this.protyleSlash = [{ this.protyleSlash = [{
id: "insert-whiteboard", id: "insert-drawing",
filter: ["Insert Drawing", "Add drawing", "Insert whiteboard", "Add whiteboard", "whiteboard", "freehand", "graphics", "jsdraw"], filter: ["Insert Drawing", "Add drawing", "whiteboard", "freehand", "graphics", "jsdraw"],
html: getMenuHTML("iconDraw", this.i18n.insertWhiteboard), html: getMenuHTML("iconDraw", this.i18n.insertDrawing),
callback: async (protyle: Protyle) => { callback: (protyle: Protyle) => {
void this.analytics.sendEvent('create'); void this.analytics.sendEvent('create');
const fileID = generateRandomString(); const fileID = generateRandomString();
const syncID = generateTimeString() + '-' + generateRandomString(); const syncID = generateTimeString() + '-' + generateRandomString();
protyle.insert(getMarkdownBlock(fileID, syncID), true, false); protyle.insert(getMarkdownBlock(fileID, syncID), true, false);
(await EditorManager.create(fileID, this)).open(this); new EditorManager(fileID, this.config.getDefaultEditorOptions()).open(this);
} }
}]; }];
@ -43,31 +43,14 @@ export default class DrawJSPlugin extends Plugin {
if (ids === null) return; if (ids === null) return;
e.detail.menu.addItem({ e.detail.menu.addItem({
icon: "iconDraw", icon: "iconDraw",
label: this.i18n.editWhiteboard, label: "Edit with js-draw",
click: async () => { click: () => {
void this.analytics.sendEvent('edit'); void this.analytics.sendEvent('edit');
(await EditorManager.create(ids.fileID, this)).open(this); new EditorManager(ids.fileID, this.config.getDefaultEditorOptions()).open(this);
} }
}) })
}) })
this.addCommand({
langKey: "editShortcut",
hotkey: "⌥⇧D",
callback: async () => {
this.editSelectedImg().catch(e => ErrorReporter.error(e, 5000));
},
})
this.addTopBar({
icon: "iconDraw",
title: this.i18n.editShortcut,
callback: async () => {
await this.editSelectedImg().catch(e => ErrorReporter.error(e, 5000));
},
position: "left"
})
} }
onunload() { onunload() {
@ -78,22 +61,6 @@ export default class DrawJSPlugin extends Plugin {
void this.analytics.sendEvent("uninstall"); void this.analytics.sendEvent("uninstall");
} }
private async editSelectedImg() {
let selectedImg = document.getElementsByClassName('img--select');
if(selectedImg.length == 0) {
throw new MustSelectError();
}
let ids = imgSrcToIDs(findImgSrc(selectedImg[0] as HTMLElement));
if(ids == null) {
throw new NotAWhiteboardError();
}
void this.analytics.sendEvent('edit');
(await EditorManager.create(ids.fileID, this)).open(this);
}
private async startConfig() { private async startConfig() {
this.config = new PluginConfig(); this.config = new PluginConfig();
await this.config.load(); await this.config.load();

65
src/migration.ts Normal file
View file

@ -0,0 +1,65 @@
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 [];
}
}

View file

@ -1,6 +1,5 @@
import {getBlockByID, sql, updateBlock} from "@/api"; import {getBlockByID, sql, updateBlock} from "@/api";
import {assetPathToIDs, IDsToAssetPath} from "@/helper"; import {assetPathToIDs, IDsToAssetPath} from "@/helper";
import {MultipleSyncIDsError} from "@/errors";
export async function findSyncIDInProtyle(fileID: string, iter?: number): Promise<string> { export async function findSyncIDInProtyle(fileID: string, iter?: number): Promise<string> {
@ -16,7 +15,11 @@ export async function findSyncIDInProtyle(fileID: string, iter?: number): Promis
if(syncID == null) { if(syncID == null) {
syncID = ids.syncID; syncID = ids.syncID;
}else if(ids.syncID !== syncID) { }else if(ids.syncID !== syncID) {
throw new MultipleSyncIDsError(); 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."
);
} }
} }
} }
@ -79,7 +82,7 @@ export async function replaceBlockContent(
} }
function extractImageSourcesFromMarkdown(markdown: string, mustStartWith?: string) { function extractImageSourcesFromMarkdown(markdown: string, mustStartWith?: string) {
const imageRegex = /!\[.*?\]\(([^)\s]+)(?:\s+"[^"]+")?\)/g; // only get images const imageRegex = /!\[.*?\]\((.*?)\)/g; // only get images
return Array.from(markdown.matchAll(imageRegex)) return Array.from(markdown.matchAll(imageRegex))
.map(match => match[1]) .map(match => match[1])
.filter(source => source.startsWith(mustStartWith)) // discard other images .filter(source => source.startsWith(mustStartWith)) // discard other images