Compare commits

...

12 commits
v0.3.0 ... main

Author SHA1 Message Date
d34258e6bf
Add "directly open editor" shortcut and icon
All checks were successful
Build on Push and create Release on Tag / build (push) Successful in 4m48s
2025-06-24 22:26:26 +02:00
dc15e91def
Version bump
All checks were successful
Build on Push and create Release on Tag / build (push) Successful in 36s
2025-05-15 18:19:46 +02:00
ff83c23851
Improve cursor
All checks were successful
Build on Push and create Release on Tag / build (push) Successful in 51s
2025-05-15 18:16:50 +02:00
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 366 additions and 136 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.1",
"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.1",
"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,46 @@
{ {
"insertDrawing": "Insert Drawing" "insertDrawing": "Insert Drawing",
"editDrawing": "Edit with js-draw",
"editShortcut": "Open editor directly",
"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.",
"msgMustSelect": "Select a whiteboard in your document by left-clicking it, then use this icon/shortcut to open the editor directly.",
"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: 606 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') 3 3, none;
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

@ -1,4 +1,4 @@
import {Plugin, Protyle} from 'siyuan'; import {Plugin, Protyle, showMessage} from 'siyuan';
import { import {
getMarkdownBlock, getMarkdownBlock,
loadIcons, loadIcons,
@ -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,14 +43,31 @@ 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);
} }
}) })
}) })
this.addCommand({
langKey: "editShortcut",
hotkey: "⌥⇧D",
callback: async () => {
await this.editSelectedImg();
},
})
this.addTopBar({
icon: "iconDraw",
title: this.i18n.insertDrawing,
callback: async () => {
await this.editSelectedImg();
},
position: "left"
})
} }
onunload() { onunload() {
@ -61,6 +78,23 @@ 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) {
showMessage(this.i18n.msgMustSelect, 5000, 'info');
return;
}
let ids = imgSrcToIDs(findImgSrc(selectedImg[0] as HTMLElement));
if(ids == null) {
return;
}
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();