Merge branch 'develop' into renovate/softprops-action-gh-release-2.x

This commit is contained in:
Elian Doran 2025-06-11 20:09:30 +03:00 committed by GitHub
commit fb9f5a7584
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 731 additions and 1262 deletions

View File

@ -1,49 +0,0 @@
name: CI
on:
push:
branches:
- master
pull_request:
permissions:
actions: read
contents: read
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
filter: tree:0
fetch-depth: 0
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
# This enables task distribution via Nx Cloud
# Run this command as early as possible, before dependencies are installed
# Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
# Connect your workspace by running "nx connect" and uncomment this line to enable task distribution
# - run: pnpm dlx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="e2e-ci"
# Cache node_modules
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install --with-deps
- uses: nrwl/nx-set-shas@v4
# Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
# - run: pnpm exec nx-cloud record -- echo Hello World
# Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected
# When you enable task distribution, run the e2e-ci task instead of e2e
- run: pnpm exec nx affected -t lint test build e2e

View File

@ -35,10 +35,10 @@
"chore:generate-openapi": "tsx bin/generate-openapi.js" "chore:generate-openapi": "tsx bin/generate-openapi.js"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.52.0", "@playwright/test": "1.53.0",
"@stylistic/eslint-plugin": "4.4.1", "@stylistic/eslint-plugin": "4.4.1",
"@types/express": "5.0.3", "@types/express": "5.0.3",
"@types/node": "22.15.30", "@types/node": "22.15.31",
"@types/yargs": "17.0.33", "@types/yargs": "17.0.33",
"@vitest/coverage-v8": "3.2.3", "@vitest/coverage-v8": "3.2.3",
"eslint": "9.28.0", "eslint": "9.28.0",

View File

@ -51,8 +51,7 @@
"mind-elixir": "4.6.0", "mind-elixir": "4.6.0",
"normalize.css": "8.0.1", "normalize.css": "8.0.1",
"panzoom": "9.4.3", "panzoom": "9.4.3",
"react": "19.1.0", "preact": "10.26.8",
"react-dom": "19.1.0",
"split.js": "1.6.5", "split.js": "1.6.5",
"svg-pan-zoom": "3.6.2", "svg-pan-zoom": "3.6.2",
"vanilla-js-wheel-zoom": "9.0.4" "vanilla-js-wheel-zoom": "9.0.4"
@ -64,10 +63,8 @@
"@types/leaflet": "1.9.18", "@types/leaflet": "1.9.18",
"@types/leaflet-gpx": "1.3.7", "@types/leaflet-gpx": "1.3.7",
"@types/mark.js": "8.11.12", "@types/mark.js": "8.11.12",
"@types/react": "19.1.7",
"@types/react-dom": "19.1.6",
"copy-webpack-plugin": "13.0.0", "copy-webpack-plugin": "13.0.0",
"happy-dom": "17.6.3", "happy-dom": "18.0.1",
"script-loader": "0.7.2", "script-loader": "0.7.2",
"vite-plugin-static-copy": "3.0.0" "vite-plugin-static-copy": "3.0.0"
}, },
@ -75,7 +72,9 @@
"name": "client", "name": "client",
"targets": { "targets": {
"serve": { "serve": {
"dependsOn": ["^build"] "dependsOn": [
"^build"
]
} }
} }
} }

View File

@ -3,7 +3,7 @@ declare module "*.png" {
export default path; export default path;
} }
declare module "@triliumnext/ckeditor5/emoji_definitions/en.json?url" { declare module "*?url" {
var path: string; var path: string;
export default path; export default path;
} }

View File

@ -57,6 +57,8 @@ declare global {
process?: ElectronProcess; process?: ElectronProcess;
glob?: CustomGlobals; glob?: CustomGlobals;
EXCALIDRAW_ASSET_PATH?: string;
} }
interface AutoCompleteConfig { interface AutoCompleteConfig {

View File

@ -1,16 +1,11 @@
import TypeWidget from "./type_widget.js"; import TypeWidget from "./type_widget.js";
import utils from "../../services/utils.js";
import linkService from "../../services/link.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import type FNote from "../../entities/fnote.js"; import type FNote from "../../entities/fnote.js";
import options from "../../services/options.js"; import options from "../../services/options.js";
import type { ExcalidrawElement, Theme } from "@excalidraw/excalidraw/element/types"; import type { LibraryItem } from "@excalidraw/excalidraw/types";
import type { AppState, BinaryFileData, ExcalidrawImperativeAPI, ExcalidrawProps, LibraryItem, SceneData } from "@excalidraw/excalidraw/types"; import type { Theme } from "@excalidraw/excalidraw/element/types";
import type { JSX } from "react"; import type Canvas from "./canvas_el.js";
import type React from "react"; import { CanvasContent } from "./canvas_el.js";
import type { Root } from "react-dom/client";
import "@excalidraw/excalidraw/index.css";
import asset_path from "../../asset_path.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="canvas-widget note-detail-canvas note-detail-printable note-detail"> <div class="canvas-widget note-detail-canvas note-detail-printable note-detail">
@ -28,6 +23,7 @@ const TPL = /*html*/`
.excalidraw-wrapper { .excalidraw-wrapper {
height: 100%; height: 100%;
}
:root[dir="ltr"] :root[dir="ltr"]
.excalidraw .excalidraw
@ -51,11 +47,7 @@ const TPL = /*html*/`
</div> </div>
`; `;
interface CanvasContent {
elements: ExcalidrawElement[];
files: BinaryFileData[];
appState: Partial<AppState>;
}
interface AttachmentMetadata { interface AttachmentMetadata {
title: string; title: string;
@ -107,37 +99,22 @@ interface AttachmentMetadata {
*/ */
export default class ExcalidrawTypeWidget extends TypeWidget { export default class ExcalidrawTypeWidget extends TypeWidget {
private readonly SCENE_VERSION_INITIAL: number;
private readonly SCENE_VERSION_ERROR: number;
private currentNoteId: string; private currentNoteId: string;
private currentSceneVersion: number;
private libraryChanged: boolean; private libraryChanged: boolean;
private librarycache: LibraryItem[]; private librarycache: LibraryItem[];
private attachmentMetadata: AttachmentMetadata[]; private attachmentMetadata: AttachmentMetadata[];
private themeStyle!: Theme; private themeStyle!: Theme;
private excalidrawLib!: typeof import("@excalidraw/excalidraw");
private excalidrawApi!: ExcalidrawImperativeAPI;
private excalidrawWrapperRef!: React.RefObject<HTMLElement | null>;
private $render!: JQuery<HTMLElement>; private $render!: JQuery<HTMLElement>;
private root?: Root;
private reactHandlers!: JQuery<HTMLElement>; private reactHandlers!: JQuery<HTMLElement>;
private canvasInstance!: Canvas;
constructor() { constructor() {
super(); super();
// constants
this.SCENE_VERSION_INITIAL = -1; // -1 indicates that it is fresh. excalidraw scene version is always >0
this.SCENE_VERSION_ERROR = -2; // -2 indicates error
// currently required by excalidraw, in order to allows self-hosting fonts locally.
// this avoids making excalidraw load the fonts from an external CDN.
(window as any).EXCALIDRAW_ASSET_PATH = `${window.location.pathname}/node_modules/@excalidraw/excalidraw/dist/prod`;
// temporary vars // temporary vars
this.currentNoteId = ""; this.currentNoteId = "";
this.currentSceneVersion = this.SCENE_VERSION_INITIAL;
// will be overwritten // will be overwritten
this.$render; this.$render;
@ -182,34 +159,48 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
throw new Error("Unable to find element to render."); throw new Error("Unable to find element to render.");
} }
// See https://github.com/excalidraw/excalidraw/issues/7899. const Canvas = (await import("./canvas_el.js")).default;
if (!window.process) { this.canvasInstance = new Canvas({
(window.process as any) = {}; // this makes sure that 1) manual theme switch button is hidden 2) theme stays as it should after opening menu
theme: this.themeStyle,
onChange: () => this.onChangeHandler(),
viewModeEnabled: options.is("databaseReadonly"),
zenModeEnabled: false,
gridModeEnabled: false,
isCollaborating: false,
detectScroll: false,
handleKeyboardGlobally: false,
autoFocus: false,
UIOptions: {
canvasActions: {
saveToActiveFile: false,
export: false
} }
if (!window.process.env) { },
window.process.env = {}; onLibraryChange: () => {
} this.libraryChanged = true;
(window.process.env as any).PREACT = false;
const excalidraw = await import("@excalidraw/excalidraw"); this.saveData();
this.excalidrawLib = excalidraw; },
});
const { createRoot } = await import("react-dom/client"); await setupFonts();
const React = (await import("react")).default; this.canvasInstance.renderCanvas(renderElement);
this.root?.unmount();
this.root = createRoot(renderElement);
this.root.render(React.createElement(() => this.createExcalidrawReactApp(React, excalidraw.Excalidraw)));
} }
/** /**
* called to populate the widget container with the note content * called to populate the widget container with the note content
*/ */
async doRefresh(note: FNote) { async doRefresh(note: FNote) {
if (!this.canvasInstance) {
await this.#init();
}
// see if the note changed, since we do not get a new class for a new note // see if the note changed, since we do not get a new class for a new note
const noteChanged = this.currentNoteId !== note.noteId; const noteChanged = this.currentNoteId !== note.noteId;
if (noteChanged) { if (noteChanged) {
// reset the scene to omit unnecessary onchange handler // reset the scene to omit unnecessary onchange handler
this.currentSceneVersion = this.SCENE_VERSION_INITIAL; this.canvasInstance.resetSceneVersion();
} }
this.currentNoteId = note.noteId; this.currentNoteId = note.noteId;
@ -217,10 +208,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
const blob = await note.getBlob(); const blob = await note.getBlob();
// before we load content into excalidraw, make sure excalidraw has loaded // before we load content into excalidraw, make sure excalidraw has loaded
while (!this.excalidrawApi) { await this.canvasInstance.waitForApiToBecomeAvailable();
console.log("excalidrawApi not yet loaded, sleep 200ms...");
await utils.sleep(200);
}
/** /**
* new and empty note - make sure that canvas is empty. * new and empty note - make sure that canvas is empty.
@ -229,15 +217,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
* newly instantiated? * newly instantiated?
*/ */
if (!blob?.content?.trim()) { if (!blob?.content?.trim()) {
const sceneData: SceneData = { this.canvasInstance.resetScene(this.themeStyle);
elements: [],
appState: {
theme: this.themeStyle
}
};
// TODO: Props mismatch.
this.excalidrawApi.updateScene(sceneData as any);
} else if (blob.content) { } else if (blob.content) {
let content: CanvasContent; let content: CanvasContent;
@ -254,36 +234,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
}; };
} }
const { elements, files } = content; this.canvasInstance.loadData(content, this.themeStyle);
const appState: Partial<AppState> = content.appState ?? {};
appState.theme = this.themeStyle;
if (this.excalidrawWrapperRef.current) {
const boundingClientRect = this.excalidrawWrapperRef.current.getBoundingClientRect();
appState.width = boundingClientRect.width;
appState.height = boundingClientRect.height;
appState.offsetLeft = boundingClientRect.left;
appState.offsetTop = boundingClientRect.top;
}
const sceneData: SceneData = {
elements,
appState
};
// files are expected in an array when loading. they are stored as a key-index object
// see example for loading here:
// https://github.com/excalidraw/excalidraw/blob/c5a7723185f6ca05e0ceb0b0d45c4e3fbcb81b2a/src/packages/excalidraw/example/App.js#L68
const fileArray: BinaryFileData[] = [];
for (const fileId in files) {
const file = files[fileId];
// TODO: dataURL is replaceable with a trilium image url
// maybe we can save normal images (pasted) with base64 data url, and trilium images
// with their respective url! nice
// file.dataURL = "http://localhost:8080/api/images/ltjOiU8nwoZx/start.png";
fileArray.push(file);
}
Promise.all( Promise.all(
(await note.getAttachmentsByRole("canvasLibraryItem")).map(async (attachment) => { (await note.getAttachmentsByRole("canvasLibraryItem")).map(async (attachment) => {
@ -310,23 +261,19 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
const metadata = results.map((result) => result.metadata); const metadata = results.map((result) => result.metadata);
// Update the library and save to independent variables // Update the library and save to independent variables
this.excalidrawApi.updateLibrary({ libraryItems, merge: false }); this.canvasInstance.updateLibrary(libraryItems);
// save state of library to compare it to the new state later. // save state of library to compare it to the new state later.
this.librarycache = libraryItems; this.librarycache = libraryItems;
this.attachmentMetadata = metadata; this.attachmentMetadata = metadata;
}); });
// Update the scene
// TODO: Fix type of sceneData
this.excalidrawApi.updateScene(sceneData as any);
this.excalidrawApi.addFiles(fileArray);
this.excalidrawApi.history.clear();
} }
// set initial scene version // set initial scene version
if (this.currentSceneVersion === this.SCENE_VERSION_INITIAL) { if (this.canvasInstance.isInitialScene()) {
this.currentSceneVersion = this.getSceneVersion(); this.canvasInstance.updateSceneVersion();
} }
} }
@ -335,56 +282,14 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
* this is automatically called after this.saveData(); * this is automatically called after this.saveData();
*/ */
async getData() { async getData() {
const elements = this.excalidrawApi.getSceneElements(); const { content, svg } = await this.canvasInstance.getData();
const appState = this.excalidrawApi.getAppState(); const attachments = [{ role: "image", title: "canvas-export.svg", mime: "image/svg+xml", content: svg, position: 0 }];
/**
* A file is not deleted, even though removed from canvas. Therefore, we only keep
* files that are referenced by an element. Maybe this will change with a new excalidraw version?
*/
const files = this.excalidrawApi.getFiles();
// parallel svg export to combat bitrot and enable rendering image for note inclusion, preview, and share
const svg = await this.excalidrawLib.exportToSvg({
elements,
appState,
exportPadding: 5, // 5 px padding
files
});
const svgString = svg.outerHTML;
const activeFiles: Record<string, BinaryFileData> = {};
// TODO: Used any where upstream typings appear to be broken.
elements.forEach((element: any) => {
if ("fileId" in element && element.fileId) {
activeFiles[element.fileId] = files[element.fileId];
}
});
const content = {
type: "excalidraw",
version: 2,
elements,
files: activeFiles,
appState: {
scrollX: appState.scrollX,
scrollY: appState.scrollY,
zoom: appState.zoom
}
};
const attachments = [{ role: "image", title: "canvas-export.svg", mime: "image/svg+xml", content: svgString, position: 0 }];
if (this.libraryChanged) { if (this.libraryChanged) {
// this.libraryChanged is unset in dataSaved() // this.libraryChanged is unset in dataSaved()
// there's no separate method to get library items, so have to abuse this one // there's no separate method to get library items, so have to abuse this one
const libraryItems = await this.excalidrawApi.updateLibrary({ const libraryItems = await this.canvasInstance.getLibraryItems();
libraryItems() {
return [];
},
merge: true
});
// excalidraw saves the library as a own state. the items are saved to libraryItems. then we compare the library right now with a libraryitemcache. The cache is filled when we first load the Library into the note. // excalidraw saves the library as a own state. the items are saved to libraryItems. then we compare the library right now with a libraryitemcache. The cache is filled when we first load the Library into the note.
//We need the cache to delete old attachments later in the server. //We need the cache to delete old attachments later in the server.
@ -453,146 +358,39 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
} }
// changeHandler is called upon any tiny change in excalidraw. button clicked, hover, etc. // changeHandler is called upon any tiny change in excalidraw. button clicked, hover, etc.
// make sure only when a new element is added, we actually save something. // make sure only when a new element is added, we actually save something.
const isNewSceneVersion = this.isNewSceneVersion(); const isNewSceneVersion = this.canvasInstance.isNewSceneVersion();
/** /**
* FIXME: however, we might want to make an exception, if viewport changed, since viewport * FIXME: however, we might want to make an exception, if viewport changed, since viewport
* is desired to save? (add) and appState background, and some things * is desired to save? (add) and appState background, and some things
*/ */
// upon updateScene, onchange is called, even though "nothing really changed" that is worth saving // upon updateScene, onchange is called, even though "nothing really changed" that is worth saving
const isNotInitialScene = this.currentSceneVersion !== this.SCENE_VERSION_INITIAL; const isNotInitialScene = !this.canvasInstance.isInitialScene();
const shouldSave = isNewSceneVersion && isNotInitialScene; const shouldSave = isNewSceneVersion && isNotInitialScene;
if (shouldSave) { if (shouldSave) {
this.updateSceneVersion(); this.canvasInstance.updateSceneVersion();
this.saveData(); this.saveData();
} }
} }
createExcalidrawReactApp(react: typeof React, excalidrawComponent: React.MemoExoticComponent<(props: ExcalidrawProps) => JSX.Element>) {
const excalidrawWrapperRef = react.useRef<HTMLElement>(null);
this.excalidrawWrapperRef = excalidrawWrapperRef;
const [dimensions, setDimensions] = react.useState<{ width?: number; height?: number }>({
width: undefined,
height: undefined
});
react.useEffect(() => {
if (excalidrawWrapperRef.current) {
const dimensions = {
width: excalidrawWrapperRef.current.getBoundingClientRect().width,
height: excalidrawWrapperRef.current.getBoundingClientRect().height
};
setDimensions(dimensions);
} }
const onResize = () => { async function setupFonts() {
if (this.note?.type !== "canvas") { if (window.EXCALIDRAW_ASSET_PATH) {
return; return;
} }
if (excalidrawWrapperRef.current) { // currently required by excalidraw, in order to allows self-hosting fonts locally.
const dimensions = { // this avoids making excalidraw load the fonts from an external CDN.
width: excalidrawWrapperRef.current.getBoundingClientRect().width, let path: string;
height: excalidrawWrapperRef.current.getBoundingClientRect().height if (!glob.isDev) {
}; path = `${window.location.pathname}/node_modules/@excalidraw/excalidraw/dist/prod`;
setDimensions(dimensions);
}
};
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, [excalidrawWrapperRef]);
const onLinkOpen = react.useCallback<NonNullable<ExcalidrawProps["onLinkOpen"]>>((element, event) => {
let link = element.link;
if (!link) {
return false;
}
if (link.startsWith("root/")) {
link = "#" + link;
}
const { nativeEvent } = event.detail;
event.preventDefault();
return linkService.goToLinkExt(nativeEvent, link, null);
}, []);
return react.createElement(
react.Fragment,
null,
react.createElement(
"div",
{
className: "excalidraw-wrapper",
ref: excalidrawWrapperRef
},
react.createElement(excalidrawComponent, {
// this makes sure that 1) manual theme switch button is hidden 2) theme stays as it should after opening menu
theme: this.themeStyle,
excalidrawAPI: (api: ExcalidrawImperativeAPI) => {
this.excalidrawApi = api;
},
onLibraryChange: () => {
this.libraryChanged = true;
this.saveData();
},
onChange: () => this.onChangeHandler(),
viewModeEnabled: options.is("databaseReadonly"),
zenModeEnabled: false,
gridModeEnabled: false,
isCollaborating: false,
detectScroll: false,
handleKeyboardGlobally: false,
autoFocus: false,
onLinkOpen,
UIOptions: {
canvasActions: {
saveToActiveFile: false,
export: false
}
}
})
)
);
}
/**
* needed to ensure, that multipleOnChangeHandler calls do not trigger a save.
* we compare the scene version as suggested in:
* https://github.com/excalidraw/excalidraw/issues/3014#issuecomment-778115329
*
* info: sceneVersions are not incrementing. it seems to be a pseudo-random number
*/
isNewSceneVersion() {
if (options.is("databaseReadonly")) {
return false;
}
const sceneVersion = this.getSceneVersion();
return (
this.currentSceneVersion === this.SCENE_VERSION_INITIAL || // initial scene version update
this.currentSceneVersion !== sceneVersion
); // ensure scene changed
}
getSceneVersion() {
if (this.excalidrawApi) {
const elements = this.excalidrawApi.getSceneElements();
return this.excalidrawLib.getSceneVersion(elements);
} else { } else {
return this.SCENE_VERSION_ERROR; path = (await import("../../../node_modules/@excalidraw/excalidraw/dist/prod/fonts/Excalifont/Excalifont-Regular-a88b72a24fb54c9f94e3b5fdaa7481c9.woff2?url")).default;
} let pathComponents = path.split("/");
path = pathComponents.slice(0, pathComponents.length - 2).join("/");
} }
updateSceneVersion() { window.EXCALIDRAW_ASSET_PATH = path;
this.currentSceneVersion = this.getSceneVersion();
}
} }

View File

@ -0,0 +1,179 @@
import "@excalidraw/excalidraw/index.css";
import { Excalidraw, getSceneVersion, exportToSvg } from "@excalidraw/excalidraw";
import { createElement, render, unmountComponentAtNode } from "preact/compat";
import { AppState, BinaryFileData, ExcalidrawImperativeAPI, ExcalidrawProps, LibraryItem } from "@excalidraw/excalidraw/types";
import type { ComponentType } from "preact";
import { ExcalidrawElement, NonDeletedExcalidrawElement, Theme } from "@excalidraw/excalidraw/element/types";
export interface CanvasContent {
elements: ExcalidrawElement[];
files: BinaryFileData[];
appState: Partial<AppState>;
}
/** Indicates that it is fresh. excalidraw scene version is always >0 */
const SCENE_VERSION_INITIAL = -1;
export default class Canvas {
private currentSceneVersion: number;
private opts: ExcalidrawProps;
private excalidrawApi!: ExcalidrawImperativeAPI;
private initializedPromise: JQuery.Deferred<void>;
constructor(opts: ExcalidrawProps) {
this.opts = opts;
this.currentSceneVersion = SCENE_VERSION_INITIAL;
this.initializedPromise = $.Deferred();
}
renderCanvas(targetEl: HTMLElement) {
unmountComponentAtNode(targetEl);
render(this.createCanvasElement({
...this.opts,
excalidrawAPI: (api: ExcalidrawImperativeAPI) => {
this.excalidrawApi = api;
this.initializedPromise.resolve();
},
}), targetEl);
}
async waitForApiToBecomeAvailable() {
while (!this.excalidrawApi) {
await this.initializedPromise;
}
}
private createCanvasElement(opts: ExcalidrawProps) {
return createElement("div", { className: "excalidraw-wrapper", },
createElement(Excalidraw as ComponentType<ExcalidrawProps>, opts)
);
}
/**
* needed to ensure, that multipleOnChangeHandler calls do not trigger a save.
* we compare the scene version as suggested in:
* https://github.com/excalidraw/excalidraw/issues/3014#issuecomment-778115329
*
* info: sceneVersions are not incrementing. it seems to be a pseudo-random number
*/
isNewSceneVersion() {
const sceneVersion = this.getSceneVersion();
return (
this.currentSceneVersion === SCENE_VERSION_INITIAL || // initial scene version update
this.currentSceneVersion !== sceneVersion
); // ensure scene changed
}
getSceneVersion() {
const elements = this.excalidrawApi.getSceneElements();
return getSceneVersion(elements);
}
updateSceneVersion() {
this.currentSceneVersion = this.getSceneVersion();
}
resetSceneVersion() {
this.currentSceneVersion = SCENE_VERSION_INITIAL;
}
isInitialScene() {
return this.currentSceneVersion === SCENE_VERSION_INITIAL;
}
resetScene(theme: Theme) {
this.excalidrawApi.updateScene({
elements: [],
appState: {
theme
}
});
}
loadData(content: CanvasContent, theme: Theme) {
const { elements, files } = content;
const appState: Partial<AppState> = content.appState ?? {};
appState.theme = theme;
// files are expected in an array when loading. they are stored as a key-index object
// see example for loading here:
// https://github.com/excalidraw/excalidraw/blob/c5a7723185f6ca05e0ceb0b0d45c4e3fbcb81b2a/src/packages/excalidraw/example/App.js#L68
const fileArray: BinaryFileData[] = [];
for (const fileId in files) {
const file = files[fileId];
// TODO: dataURL is replaceable with a trilium image url
// maybe we can save normal images (pasted) with base64 data url, and trilium images
// with their respective url! nice
// file.dataURL = "http://localhost:8080/api/images/ltjOiU8nwoZx/start.png";
fileArray.push(file);
}
// Update the scene
// TODO: Fix type of sceneData
this.excalidrawApi.updateScene({
elements,
appState: appState as AppState
});
this.excalidrawApi.addFiles(fileArray);
this.excalidrawApi.history.clear();
}
async getData() {
const elements = this.excalidrawApi.getSceneElements();
const appState = this.excalidrawApi.getAppState();
/**
* A file is not deleted, even though removed from canvas. Therefore, we only keep
* files that are referenced by an element. Maybe this will change with a new excalidraw version?
*/
const files = this.excalidrawApi.getFiles();
// parallel svg export to combat bitrot and enable rendering image for note inclusion, preview, and share
const svg = await exportToSvg({
elements,
appState,
exportPadding: 5, // 5 px padding
files
});
const svgString = svg.outerHTML;
const activeFiles: Record<string, BinaryFileData> = {};
elements.forEach((element: NonDeletedExcalidrawElement) => {
if ("fileId" in element && element.fileId) {
activeFiles[element.fileId] = files[element.fileId];
}
});
const content = {
type: "excalidraw",
version: 2,
elements,
files: activeFiles,
appState: {
scrollX: appState.scrollX,
scrollY: appState.scrollY,
zoom: appState.zoom
}
};
return {
content,
svg: svgString
}
}
async getLibraryItems() {
return this.excalidrawApi.updateLibrary({
libraryItems() {
return [];
},
merge: true
});
}
async updateLibrary(libraryItems: LibraryItem[]) {
this.excalidrawApi.updateLibrary({ libraryItems, merge: false });
}
}

View File

@ -43,11 +43,22 @@ export default defineConfig(() => ({
{ {
find: "@triliumnext/highlightjs", find: "@triliumnext/highlightjs",
replacement: resolve(__dirname, "node_modules/@triliumnext/highlightjs/dist") replacement: resolve(__dirname, "node_modules/@triliumnext/highlightjs/dist")
},
{
find: "react",
replacement: "preact/compat"
},
{
find: "react-dom",
replacement: "preact/compat"
} }
], ],
dedupe: [ dedupe: [
"react", "react",
"react-dom" "react-dom",
"preact",
"preact/compat",
"preact/hooks"
] ]
}, },
// Uncomment this if you are using workers. // Uncomment this if you are using workers.
@ -97,5 +108,8 @@ export default defineConfig(() => ({
}, },
commonjsOptions: { commonjsOptions: {
transformMixedEsModules: true, transformMixedEsModules: true,
},
define: {
"process.env.IS_PREACT": JSON.stringify("true"),
} }
})); }));

View File

@ -88,13 +88,13 @@
"multer": "2.0.1", "multer": "2.0.1",
"normalize-strings": "1.1.1", "normalize-strings": "1.1.1",
"ollama": "0.5.16", "ollama": "0.5.16",
"openai": "5.2.0", "openai": "5.3.0",
"rand-token": "1.0.1", "rand-token": "1.0.1",
"safe-compare": "1.1.4", "safe-compare": "1.1.4",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"sanitize-html": "2.17.0", "sanitize-html": "2.17.0",
"sax": "1.4.1", "sax": "1.4.1",
"serve-favicon": "2.5.0", "serve-favicon": "2.5.1",
"stream-throttle": "0.1.3", "stream-throttle": "0.1.3",
"strip-bom": "5.0.0", "strip-bom": "5.0.0",
"striptags": "3.2.0", "striptags": "3.2.0",

View File

@ -40,7 +40,7 @@
"@playwright/test": "^1.36.0", "@playwright/test": "^1.36.0",
"@triliumnext/server": "workspace:*", "@triliumnext/server": "workspace:*",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/node": "22.15.30", "@types/node": "22.15.31",
"@vitest/coverage-v8": "^3.0.5", "@vitest/coverage-v8": "^3.0.5",
"@vitest/ui": "^3.0.0", "@vitest/ui": "^3.0.0",
"chalk": "5.4.1", "chalk": "5.4.1",
@ -49,7 +49,7 @@
"eslint": "^9.8.0", "eslint": "^9.8.0",
"eslint-config-prettier": "^10.0.0", "eslint-config-prettier": "^10.0.0",
"eslint-plugin-playwright": "^2.0.0", "eslint-plugin-playwright": "^2.0.0",
"happy-dom": "~17.6.0", "happy-dom": "~18.0.0",
"jiti": "2.4.2", "jiti": "2.4.2",
"jsdom": "~26.1.0", "jsdom": "~26.1.0",
"jsonc-eslint-parser": "^2.1.0", "jsonc-eslint-parser": "^2.1.0",
@ -92,6 +92,9 @@
}, },
"overrides": { "overrides": {
"node-abi": "4.9.0", "node-abi": "4.9.0",
"mermaid": "11.6.0",
"preact": "10.26.8",
"roughjs": "4.6.6",
"@types/express-serve-static-core": "5.0.6", "@types/express-serve-static-core": "5.0.6",
"flat@<5.0.1": ">=5.0.1", "flat@<5.0.1": ">=5.0.1",
"debug@>=3.2.0 <3.2.7": ">=3.2.7", "debug@>=3.2.0 <3.2.7": ">=3.2.7",

View File

@ -23,12 +23,12 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"devDependencies": { "devDependencies": {
"@digitak/esrun": "^3.2.24", "@digitak/esrun": "^3.2.24",
"@types/swagger-ui": "^3.52.0", "@types/swagger-ui": "^5.0.0",
"@typescript-eslint/eslint-plugin": "^6.7.2", "@typescript-eslint/eslint-plugin": "^6.7.2",
"@typescript-eslint/parser": "^6.7.2", "@typescript-eslint/parser": "^6.7.2",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"eslint": "^8.49.0", "eslint": "^9.0.0",
"highlight.js": "^11.8.0", "highlight.js": "^11.8.0",
"typescript": "^5.2.2" "typescript": "^5.2.2"
}, },

1373
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff