Compare commits

...

9 commits
v0.3.0 ... main

Author SHA1 Message Date
17d4e5938b
Version bump
All checks were successful
Build on Push and create Release on Tag / build (push) Successful in 36s
2025-05-09 22:57:42 +02:00
a079298433
Add CI
All checks were successful
Build on Push and create Release on Tag / build (push) Successful in 3m55s
2025-05-08 22:47:09 +02:00
5322944ad9
Add custom cursor on editor canvas 2025-05-07 21:16:50 +02:00
77e8218d1f
Config improvements and compatibility with old versions 2025-05-06 23:12:51 +02:00
764f9fe5a4
Add option to remember editor position and zoom 2025-05-06 18:19:18 +02:00
fa3eba219e
Suggest popular background colors, add transparency support
Making the UI more user-friendly by suggesting some commonly used colors in the Settings menu
2025-05-05 19:17:59 +02:00
1ad26d1e23
Add funding link 2025-05-01 23:01:55 +02:00
8d4779b8fe
Improve error handling and code structure 2025-04-23 09:52:45 +02:00
f35342a791
Start workin on i18n 2025-04-20 22:16:48 +02:00
11 changed files with 329 additions and 135 deletions

View file

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

View file

@ -1,6 +1,6 @@
{ {
"name": "siyuan-jsdraw-plugin", "name": "siyuan-jsdraw-plugin",
"version": "0.3.0", "version": "0.4.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,6 +36,7 @@
}, },
"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.3.0", "version": "0.4.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,3 +1,44 @@
{ {
"insertDrawing": "Insert Drawing" "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."
}
}
} }

BIN
public/webapp/cursor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 B

24
scripts/validate_tag.cjs Normal file
View file

@ -0,0 +1,24 @@
const fs = require('fs');
const path = require('path');
const [tagName] = process.argv.slice(2); // Get tag from CLI arguments
if (!tagName) {
console.error('Error: No tag name provided.');
process.exit(1);
}
const TAG_VERSION = tagName.replace('refs/tags/v', '');
try {
const packageJson = JSON.parse(fs.readFileSync(path.resolve('package.json'), 'utf8'));
const pluginJson = JSON.parse(fs.readFileSync(path.resolve('plugin.json'), 'utf8'));
if (TAG_VERSION !== packageJson.version || TAG_VERSION !== pluginJson.version) {
console.error(`Error: Tag version (${TAG_VERSION}) does not match package.json (${packageJson.version}) or plugin.json (${pluginJson.version})`);
process.exit(1);
}
console.log('Tag version matches both JSON files.');
} catch (err) {
console.error('Failed to read or parse JSON files:', err.message);
process.exit(1);
}

View file

@ -1,17 +1,16 @@
import {PluginFile} from "@/file"; 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, showMessage} from "siyuan";
import {SettingUtils} from "@/libs/setting-utils"; import {SettingUtils} from "@/libs/setting-utils";
import {validateColor} from "@/helper"; import {getFirstDefined} from "@/helper";
type Options = { export interface Options {
grid: boolean
background: string
dialogOnDesktop: boolean dialogOnDesktop: boolean
analytics: boolean analytics: boolean
}; editorOptions: EditorOptions
}
export type DefaultEditorOptions = { export interface EditorOptions {
restorePosition: boolean;
grid: boolean grid: boolean
background: string background: string
} }
@ -29,30 +28,23 @@ 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();
this.options = JSON.parse(this.file.getContent()); const jsonObj = JSON.parse(this.file.getContent());
if(this.options == null) { if(jsonObj == null) {
this.loadDefaultConfig(); this.firstRun = true;
} }
} // if more than one fallback, the intermediate ones are from a legacy config file version
private loadDefaultConfig() {
this.options = { this.options = {
grid: true, dialogOnDesktop: getFirstDefined(jsonObj?.dialogOnDesktop, false),
background: "#000000", analytics: getFirstDefined(jsonObj?.analytics, true),
dialogOnDesktop: false, editorOptions: {
analytics: true, restorePosition: getFirstDefined(jsonObj?.editorOptions?.restorePosition, jsonObj?.restorePosition, true),
grid: getFirstDefined(jsonObj?.editorOptions?.grid, jsonObj?.grid, true),
background: getFirstDefined(jsonObj?.editorOptions?.background, jsonObj?.background, "#00000000")
},
}; };
this.firstRun = true;
} }
async save() { async save() {
@ -61,14 +53,16 @@ export class PluginConfig {
} }
setConfig(config: Options) { setConfig(config: Options) {
if(!validateColor(config.background)) {
alert("Invalid background color! Please enter an HEX color, like #000000 (black) or #FFFFFF (white)");
config.background = this.options.background;
}
this.options = config; 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 { export class PluginConfigViewer {
@ -76,62 +70,100 @@ 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)) {
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() { populateSettingMenu() {
this.settingUtils = new SettingUtils({ this.settingUtils = new SettingUtils({
plugin: this.plugin, plugin: this.plugin,
name: 'optionsUI',
callback: async (data) => { callback: async (data) => {
this.config.setConfig({ await this.configSaveCallback(data);
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: "Enable grid by default", title: this.plugin.i18n.settings.grid.title,
description: "Enable to automatically turn on the grid on new drawings.", description: this.plugin.i18n.settings.grid.description,
value: this.config.options.grid, value: this.config.options.editorOptions.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: "Default background Color", title: this.plugin.i18n.settings.background.title,
description: "Default background color of the drawing area for new drawings in hexadecimal.", description: this.plugin.i18n.settings.background.description,
value: this.config.options.background, value: this.config.options.editorOptions.background,
type: 'textarea', 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({ this.settingUtils.addItem({
key: "dialogOnDesktop", key: "dialogOnDesktop",
title: "Open editor as dialog on desktop", title: this.plugin.i18n.settings.dialogOnDesktop.title,
description: ` description: this.plugin.i18n.settings.dialogOnDesktop.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: "Analytics", title: this.plugin.i18n.settings.analytics.title,
description: ` description: this.plugin.i18n.settings.analytics.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,12 +1,21 @@
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, {BackgroundComponentBackgroundType, BaseWidget, Color4, EditorEventType} from "js-draw"; import Editor, {
import {Dialog, getFrontend, openTab, Plugin} from "siyuan"; BackgroundComponentBackgroundType,
BaseWidget,
Color4,
EditorEventType,
Mat33,
Vec2,
Viewport
} from "js-draw";
import {Dialog, getFrontend, openTab, Plugin, showMessage} from "siyuan";
import {findSyncIDInProtyle, replaceSyncID} from "@/protyle"; import {findSyncIDInProtyle, replaceSyncID} from "@/protyle";
import DrawJSPlugin from "@/index"; import DrawJSPlugin from "@/index";
import {DefaultEditorOptions} from "@/config"; import {EditorOptions} from "@/config";
import 'js-draw/styles'; import 'js-draw/styles';
import {SyncIDNotFoundError, UnchangedProtyleError} from "@/errors";
export class PluginEditor { export class PluginEditor {
@ -23,8 +32,9 @@ 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; }
constructor(fileID: string, defaultEditorOptions: DefaultEditorOptions) { private constructor(fileID: string) {
this.fileID = fileID; this.fileID = fileID;
@ -34,61 +44,87 @@ export class PluginEditor {
iconProvider: new MaterialIconProvider(), iconProvider: new MaterialIconProvider(),
}); });
this.genToolbar().then(() => { const styleElement = document.createElement('style');
this.editor.dispatch(this.editor.setBackgroundStyle({ autoresize: true }), false); styleElement.innerHTML = `
this.editor.getRootElement().style.height = '100%'; canvas.wetInkCanvas {
}); cursor: url('/plugins/siyuan-jsdraw-plugin/webapp/cursor.png') 6 6, auto;
findSyncIDInProtyle(this.fileID).then(async (syncID) => {
if(syncID == null) {
alert(
"Couldn't find SyncID in protyle for this file.\n" +
"Make sure the drawing you're trying to edit exists in a note.\n" +
"Close this editor tab now, and try to open the editor again."
);
return;
} }
`;
this.element.appendChild(styleElement);
this.syncID = syncID; this.editor.dispatch(this.editor.setBackgroundStyle({ autoresize: true }), false);
// restore drawing this.editor.getRootElement().style.height = '100%';
this.drawingFile = new PluginAsset(this.fileID, syncID, SVG_MIME);
await this.drawingFile.loadFromSiYuanFS();
if(this.drawingFile.getContent() != null) {
await this.editor.loadFromSVG(this.drawingFile.getContent());
}else{
// it's a new drawing
this.editor.dispatch(this.editor.setBackgroundStyle({
color: Color4.fromHex(defaultEditorOptions.background),
type: defaultEditorOptions.grid ? BackgroundComponentBackgroundType.Grid : BackgroundComponentBackgroundType.SolidColor,
autoresize: true
}));
}
}).catch((error) => {
alert("Error loading drawing: " + error);
});
} }
private async genToolbar() { 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(); 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());
@ -103,13 +139,17 @@ 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 Error("Couldn't replace old images in protyle"); if(!changed) throw new UnchangedProtyleError();
await this.drawingFile.removeOld(oldSyncID); await this.drawingFile.removeOld(oldSyncID);
} }
saveButton.setDisabled(true); saveButton.setDisabled(true);
@ -117,7 +157,10 @@ export class PluginEditor {
saveButton.setDisabled(false); saveButton.setDisabled(false);
}, 500); }, 500);
} catch (error) { } catch (error) {
alert("Error saving! The current drawing has been copied to your clipboard. You may need to create a new drawing and paste it there."); showMessage("Error saving! The current drawing has been copied to your clipboard. You may need to create a new drawing and paste it there.", 0, 'error');
if(error instanceof UnchangedProtyleError) {
showMessage("Make sure the image you're trying to edit still exists in your documents.", 0, 'error');
}
await navigator.clipboard.writeText(svgElem.outerHTML); await navigator.clipboard.writeText(svgElem.outerHTML);
console.error(error); console.error(error);
console.log("Couldn't save SVG: ", svgElem.outerHTML) console.log("Couldn't save SVG: ", svgElem.outerHTML)
@ -133,31 +176,52 @@ export class PluginEditor {
export class EditorManager { export class EditorManager {
private editor: PluginEditor private editor: PluginEditor
setEditor(editor: PluginEditor) { this.editor = editor;}
constructor(fileID: string, defaultEditorOptions: DefaultEditorOptions) { static async create(fileID: string, p: DrawJSPlugin) {
this.editor = new PluginEditor(fileID, defaultEditorOptions); 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) { static registerTab(p: DrawJSPlugin) {
p.addTab({ p.addTab({
'type': "whiteboard", 'type': "whiteboard",
init() { async init() {
const fileID = this.data.fileID; const fileID = this.data.fileID;
if (fileID == null) { if (fileID == null) {
alert("File ID missing - couldn't open file.") alert(p.i18n.errNoFileID);
return; return;
} }
const editor = new PluginEditor(fileID, p.config.getDefaultEditorOptions()); try {
this.element.appendChild(editor.getElement()); 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) { toTab(p: Plugin) {
openTab({ openTab({
app: p.app, app: p.app,
custom: { custom: {
title: 'Drawing', title: p.i18n.drawing,
icon: 'iconDraw', icon: 'iconDraw',
id: "siyuan-jsdraw-pluginwhiteboard", id: "siyuan-jsdraw-pluginwhiteboard",
data: { data: {
@ -176,7 +240,7 @@ export class EditorManager {
dialog.element.querySelector("#DrawingPanel").appendChild(this.editor.getElement()); dialog.element.querySelector("#DrawingPanel").appendChild(this.editor.getElement());
} }
async open(p: DrawJSPlugin) { 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 {

12
src/errors.ts Normal file
View file

@ -0,0 +1,12 @@
export class SyncIDNotFoundError extends Error {
readonly fileID: string;
constructor(fileID: string) {
super(`SyncID not found for file ${fileID}`);
this.fileID = fileID;
Object.setPrototypeOf(this, new.target.prototype);
}
}
export class UnchangedProtyleError extends Error {}

View file

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

View file

@ -29,12 +29,12 @@ export default class DrawJSPlugin extends Plugin {
id: "insert-drawing", id: "insert-drawing",
filter: ["Insert Drawing", "Add drawing", "whiteboard", "freehand", "graphics", "jsdraw"], filter: ["Insert Drawing", "Add drawing", "whiteboard", "freehand", "graphics", "jsdraw"],
html: getMenuHTML("iconDraw", this.i18n.insertDrawing), html: getMenuHTML("iconDraw", this.i18n.insertDrawing),
callback: (protyle: Protyle) => { callback: async (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);
new EditorManager(fileID, this.config.getDefaultEditorOptions()).open(this); (await EditorManager.create(fileID, this)).open(this);
} }
}]; }];
@ -43,10 +43,10 @@ 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: "Edit with js-draw", label: this.i18n.editDrawing,
click: () => { click: async () => {
void this.analytics.sendEvent('edit'); void this.analytics.sendEvent('edit');
new EditorManager(ids.fileID, this.config.getDefaultEditorOptions()).open(this); (await EditorManager.create(ids.fileID, this)).open(this);
} }
}) })
}) })