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/package.json b/package.json index 76504eb..f355588 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "siyuan-jsdraw-plugin", - "version": "0.4.0", + "version": "0.3.0", "type": "module", "description": "Include a whiteboard for freehand drawing anywhere in your documents.", "repository": "https://git.massive.box/massivebox/siyuan-jsdraw-plugin", @@ -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..3f18451 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.3.0", "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.
This setting is only applied if \"Background Color\" is set to \"Custom\"!" - }, - "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).
The editor will always open as a dialog on mobile." - }, - "analytics": { - "title": "Analytics", - "description": "Enable to send anonymous usage data to the developer. Privacy Policy" - }, - "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/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/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/config.ts b/src/config.ts index 7c4bfca..52fd764 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,16 +1,17 @@ import {PluginFile} from "@/file"; import {CONFIG_FILENAME, JSON_MIME, STORAGE_PATH} from "@/const"; -import {Plugin, showMessage} from "siyuan"; +import {Plugin} from "siyuan"; import {SettingUtils} from "@/libs/setting-utils"; -import {getFirstDefined} from "@/helper"; +import {validateColor} from "@/helper"; -export interface Options { +type Options = { + grid: boolean + background: string dialogOnDesktop: boolean analytics: boolean - editorOptions: EditorOptions -} -export interface EditorOptions { - restorePosition: boolean; +}; + +export type DefaultEditorOptions = { grid: boolean background: string } @@ -28,23 +29,30 @@ export class PluginConfig { this.file = new PluginFile(STORAGE_PATH, CONFIG_FILENAME, JSON_MIME); } + getDefaultEditorOptions(): DefaultEditorOptions { + return { + grid: this.options.grid, + background: this.options.background, + }; + } + async load() { this.firstRun = false; await this.file.loadFromSiYuanFS(); - const jsonObj = JSON.parse(this.file.getContent()); - if(jsonObj == null) { - this.firstRun = true; + this.options = JSON.parse(this.file.getContent()); + if(this.options == null) { + this.loadDefaultConfig(); } - // if more than one fallback, the intermediate ones are from a legacy config file version + } + + private loadDefaultConfig() { 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") - }, + grid: true, + background: "#000000", + dialogOnDesktop: false, + analytics: true, }; + this.firstRun = true; } async save() { @@ -53,14 +61,12 @@ export class PluginConfig { } 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) { - hex = hex.replace('#', ''); - return typeof hex === 'string' - && (hex.length === 6 || hex.length === 8) - && !isNaN(Number('0x' + hex)) + this.options = config; } } @@ -70,100 +76,62 @@ 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.config.setConfig({ + grid: data.grid, + background: data.background, + dialogOnDesktop: data.dialogOnDesktop, + analytics: data.analytics, + }); + await this.config.save(); } }); 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, + title: "Enable grid by default", + description: "Enable to automatically turn on the grid on new drawings.", + value: this.config.options.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' + title: "Default background Color", + description: "Default background color of the drawing area for new drawings in hexadecimal.", + value: this.config.options.background, + type: 'textarea', }); this.settingUtils.addItem({ key: "dialogOnDesktop", - title: this.plugin.i18n.settings.dialogOnDesktop.title, - description: this.plugin.i18n.settings.dialogOnDesktop.description, + 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).
+ The editor will always open as a dialog on mobile. + `, 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, + title: "Analytics", + description: ` + Enable to send anonymous usage data to the developer. + Privacy + `, value: this.config.options.analytics, type: 'checkbox' }); diff --git a/src/editor.ts b/src/editor.ts index 1331e88..8680a40 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -1,21 +1,12 @@ 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 Editor, {BackgroundComponentBackgroundType, BaseWidget, Color4, EditorEventType} from "js-draw"; +import {Dialog, getFrontend, openTab, Plugin} from "siyuan"; import {findSyncIDInProtyle, replaceSyncID} from "@/protyle"; import DrawJSPlugin from "@/index"; -import {EditorOptions} from "@/config"; +import {DefaultEditorOptions} from "@/config"; import 'js-draw/styles'; -import {SyncIDNotFoundError, UnchangedProtyleError} from "@/errors"; export class PluginEditor { @@ -32,9 +23,8 @@ export class PluginEditor { 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) { + constructor(fileID: string, defaultEditorOptions: DefaultEditorOptions) { this.fileID = fileID; @@ -44,87 +34,61 @@ export class PluginEditor { 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.genToolbar().then(() => { + this.editor.dispatch(this.editor.setBackgroundStyle({ autoresize: true }), false); + this.editor.getRootElement().style.height = '100%'; + }); - this.editor.dispatch(this.editor.setBackgroundStyle({ autoresize: true }), false); - this.editor.getRootElement().style.height = '100%'; + findSyncIDInProtyle(this.fileID).then(async (syncID) => { - } - - static async create(fileID: string, defaultEditorOptions: EditorOptions): Promise { - - 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){} + 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; } - }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 - })); - } + this.syncID = syncID; + // restore drawing + 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); + }); } - async genToolbar() { + private async genToolbar() { 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 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()); @@ -139,17 +103,13 @@ export class PluginEditor { 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(); + if(!changed) throw new Error("Couldn't replace old images in protyle"); await this.drawingFile.removeOld(oldSyncID); } saveButton.setDisabled(true); @@ -157,10 +117,7 @@ export class PluginEditor { 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'); - } + alert("Error saving! The current drawing has been copied to your clipboard. You may need to create a new drawing and paste it there."); await navigator.clipboard.writeText(svgElem.outerHTML); console.error(error); console.log("Couldn't save SVG: ", svgElem.outerHTML) @@ -176,52 +133,31 @@ export class PluginEditor { 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; + constructor(fileID: string, defaultEditorOptions: DefaultEditorOptions) { + this.editor = new PluginEditor(fileID, defaultEditorOptions); } static registerTab(p: DrawJSPlugin) { p.addTab({ 'type': "whiteboard", - async init() { + init() { const fileID = this.data.fileID; if (fileID == null) { - alert(p.i18n.errNoFileID); + alert("File ID missing - couldn't open file.") return; } - try { - const editor = await PluginEditor.create(fileID, p.config.options.editorOptions); - this.element.appendChild(editor.getElement()); - }catch (error){ - EditorManager.handleCreationError(error, p); - } + const editor = new PluginEditor(fileID, p.config.getDefaultEditorOptions()); + this.element.appendChild(editor.getElement()); } }); } - 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, + title: 'Drawing', icon: 'iconDraw', id: "siyuan-jsdraw-pluginwhiteboard", data: { @@ -240,7 +176,7 @@ export class EditorManager { dialog.element.querySelector("#DrawingPanel").appendChild(this.editor.getElement()); } - open(p: DrawJSPlugin) { + async open(p: DrawJSPlugin) { if(getFrontend() != "mobile" && !p.config.options.dialogOnDesktop) { this.toTab(p); } else { 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/helper.ts b/src/helper.ts index 7041ba5..5a08636 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -107,10 +107,9 @@ export function imgSrcToIDs(imgSrc: string | null): { fileID: string; syncID: st } -export function getFirstDefined(...a) { - for(let i = 0; i < a.length; i++) { - if(a[i] !== undefined) { - return a[i]; - } - } +export function validateColor(hex: string) { + hex = hex.replace('#', ''); + return typeof hex === 'string' + && hex.length === 6 + && !isNaN(Number('0x' + hex)) } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 9d02d6d..599bf3a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,12 +29,12 @@ export default class DrawJSPlugin extends Plugin { id: "insert-drawing", filter: ["Insert Drawing", "Add drawing", "whiteboard", "freehand", "graphics", "jsdraw"], html: getMenuHTML("iconDraw", this.i18n.insertDrawing), - callback: async (protyle: Protyle) => { + callback: (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); + new EditorManager(fileID, this.config.getDefaultEditorOptions()).open(this); } }]; @@ -43,10 +43,10 @@ export default class DrawJSPlugin extends Plugin { if (ids === null) return; e.detail.menu.addItem({ icon: "iconDraw", - label: this.i18n.editDrawing, - click: async () => { + label: "Edit with js-draw", + click: () => { void this.analytics.sendEvent('edit'); - (await EditorManager.create(ids.fileID, this)).open(this); + new EditorManager(ids.fileID, this.config.getDefaultEditorOptions()).open(this); } }) })