mirror of
https://github.com/zadam/trilium.git
synced 2025-10-26 07:46:30 +01:00
Compare commits
20 Commits
v0.61.6-be
...
v0.61.7-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48248224f8 | ||
|
|
79e5e3b65f | ||
|
|
d0383f3044 | ||
|
|
62ecd1ad76 | ||
|
|
75b7706c2c | ||
|
|
584b176643 | ||
|
|
a16555bbff | ||
|
|
a749e24147 | ||
|
|
bb81f110dd | ||
|
|
5dd6f49104 | ||
|
|
bc6be44b19 | ||
|
|
8075265753 | ||
|
|
4a67f63abd | ||
|
|
d9666210f1 | ||
|
|
5f6d562bf8 | ||
|
|
f37dc66074 | ||
|
|
0b84524807 | ||
|
|
8062bb7e2a | ||
|
|
806062c8d1 | ||
|
|
83f19c0537 |
@@ -75,7 +75,6 @@ module.exports = {
|
||||
glob: true,
|
||||
log: true,
|
||||
EditorWatchdog: true,
|
||||
// \src\share\canvas_share.js
|
||||
React: true,
|
||||
appState: true,
|
||||
ExcalidrawLib: true,
|
||||
|
||||
@@ -2,7 +2,7 @@ image:
|
||||
file: .gitpod.dockerfile
|
||||
|
||||
tasks:
|
||||
- before: nvm install 18.16.1 && nvm use 18.16.1
|
||||
- before: nvm install 18.18.0 && nvm use 18.18.0
|
||||
init: npm install
|
||||
command: npm run start-server
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# !!! Don't try to build this Dockerfile directly, run it through bin/build-docker.sh script !!!
|
||||
FROM node:18.16.1-alpine
|
||||
FROM node:18.18.0-alpine
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
@@ -5,18 +5,3 @@ echo "Packaging debian x64 distribution..."
|
||||
VERSION=`jq -r ".version" package.json`
|
||||
|
||||
./node_modules/.bin/electron-installer-debian --config bin/deb-options.json --options.version=${VERSION} --arch amd64
|
||||
|
||||
|
||||
# hacky stop-gag measure to produce debian compatible XZ compressed debs until this is fixed: https://github.com/electron-userland/electron-installer-debian/issues/272
|
||||
cd dist
|
||||
ar x trilium_${VERSION}_amd64.deb
|
||||
rm trilium_${VERSION}_amd64.deb
|
||||
# recompress
|
||||
< control.tar.zst zstd -d | xz > control.tar.xz
|
||||
< data.tar.zst zstd -d | xz > data.tar.xz
|
||||
# create deb archive (I really do not know, what argument "sdsd" is for but something is required for ar to create the archive as desired)
|
||||
ar -m -c -a sdsd trilium_${VERSION}_amd64.deb debian-binary control.tar.xz data.tar.xz
|
||||
|
||||
rm control* data* debian-binary
|
||||
|
||||
echo "Converted to XZ deb"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PKG_DIR=dist/trilium-linux-x64-server
|
||||
NODE_VERSION=18.16.1
|
||||
NODE_VERSION=18.18.0
|
||||
|
||||
if [ "$1" != "DONTCOPY" ]
|
||||
then
|
||||
|
||||
@@ -5,7 +5,7 @@ if [[ $# -eq 0 ]] ; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
n exec 18.16.1 npm run webpack
|
||||
n exec 18.18.0 npm run webpack
|
||||
|
||||
DIR=$1
|
||||
|
||||
@@ -27,7 +27,7 @@ cp -r electron.js $DIR/
|
||||
cp webpack-* $DIR/
|
||||
|
||||
# run in subshell (so we return to original dir)
|
||||
(cd $DIR && n exec 18.16.1 npm install --only=prod)
|
||||
(cd $DIR && n exec 18.18.0 npm install --only=prod)
|
||||
|
||||
# cleanup of useless files in dependencies
|
||||
rm -r $DIR/node_modules/image-q/demo
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"src": "dist/trilium-linux-x64",
|
||||
"dest": "dist/",
|
||||
"compression": "xz",
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"genericName": "Note taker",
|
||||
@@ -11,4 +12,4 @@
|
||||
"bin": "trilium",
|
||||
"icon": "dist/trilium-linux-x64/icon.png",
|
||||
"categories": [ "Office" ]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ appIconService.installLocalAppIcon();
|
||||
|
||||
require('electron-dl')({ saveAs: true });
|
||||
|
||||
// needed for excalidraw export https://github.com/zadam/trilium/issues/4271
|
||||
app.commandLine.appendSwitch("enable-experimental-web-platform-features");
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
|
||||
293
libraries/mermaid.min.js
vendored
293
libraries/mermaid.min.js
vendored
File diff suppressed because one or more lines are too long
120
package-lock.json
generated
120
package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"version": "0.61.5-beta",
|
||||
"version": "0.61.6-beta",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "trilium",
|
||||
"version": "0.61.5-beta",
|
||||
"version": "0.61.6-beta",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "6.0.4",
|
||||
"@electron/remote": "2.0.11",
|
||||
"@excalidraw/excalidraw": "0.15.3",
|
||||
"@excalidraw/excalidraw": "0.16.1",
|
||||
"archiver": "5.3.1",
|
||||
"async-mutex": "0.4.0",
|
||||
"axios": "1.5.0",
|
||||
@@ -22,7 +22,7 @@
|
||||
"compression": "1.7.4",
|
||||
"cookie-parser": "1.4.6",
|
||||
"csurf": "1.11.0",
|
||||
"dayjs": "1.11.9",
|
||||
"dayjs": "1.11.10",
|
||||
"dayjs-plugin-utc": "0.1.2",
|
||||
"debounce": "1.2.1",
|
||||
"ejs": "3.1.9",
|
||||
@@ -47,7 +47,7 @@
|
||||
"jimp": "0.22.10",
|
||||
"joplin-turndown-plugin-gfm": "1.0.12",
|
||||
"jsdom": "22.1.0",
|
||||
"marked": "8.0.1",
|
||||
"marked": "9.0.3",
|
||||
"mime-types": "2.1.35",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"node-abi": "3.47.0",
|
||||
@@ -70,7 +70,7 @@
|
||||
"tmp": "0.2.1",
|
||||
"turndown": "7.1.2",
|
||||
"unescape": "1.0.1",
|
||||
"ws": "8.14.0",
|
||||
"ws": "8.14.2",
|
||||
"xml2js": "0.6.2",
|
||||
"yauzl": "2.10.0"
|
||||
},
|
||||
@@ -79,11 +79,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "25.8.0",
|
||||
"electron": "25.8.1",
|
||||
"electron-builder": "24.6.4",
|
||||
"electron-packager": "17.1.2",
|
||||
"electron-rebuild": "3.2.9",
|
||||
"eslint": "8.48.0",
|
||||
"eslint": "8.49.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "9.0.0",
|
||||
"eslint-plugin-import": "2.28.1",
|
||||
@@ -447,18 +447,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz",
|
||||
"integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==",
|
||||
"version": "8.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz",
|
||||
"integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@excalidraw/excalidraw": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.15.3.tgz",
|
||||
"integrity": "sha512-/gpY7fgMO/AEaFLWnPqzbY8H7ly+/zocFf7D0Is5sWNMD2mhult5tana12lXKLSJ6EAz7ubo1A7LajXzvJXJDA==",
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.16.1.tgz",
|
||||
"integrity": "sha512-4zirHk7dNx6SVq2jQmYOLliqAa1h3WPVqHM5qtJyhD769VsOqwlkopAcnZMb3G1PeIMm6cf2F31quS5MVqvoOQ==",
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.2 || ^18.2.0",
|
||||
"react-dom": "^17.0.2 || ^18.2.0"
|
||||
@@ -471,9 +471,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
|
||||
"integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
|
||||
"version": "0.11.11",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
|
||||
"integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@humanwhocodes/object-schema": "^1.2.1",
|
||||
@@ -3836,9 +3836,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.9",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
|
||||
"integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA=="
|
||||
"version": "1.11.10",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
|
||||
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
|
||||
},
|
||||
"node_modules/dayjs-plugin-utc": {
|
||||
"version": "0.1.2",
|
||||
@@ -4276,9 +4276,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "25.8.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-25.8.0.tgz",
|
||||
"integrity": "sha512-T3kC1a/3ntSaYMCVVfUUc9v7myPzi6J2GP0Ad/CyfWKDPp054dGyKxb2EEjKnxQQ7wfjsT1JTEdBG04x6ekVBw==",
|
||||
"version": "25.8.1",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-25.8.1.tgz",
|
||||
"integrity": "sha512-GtcP1nMrROZfFg0+mhyj1hamrHvukfF6of2B/pcWxmWkd5FVY1NJib0tlhiorFZRzQN5Z+APLPr7aMolt7i2AQ==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
@@ -5001,16 +5001,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz",
|
||||
"integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==",
|
||||
"version": "8.49.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz",
|
||||
"integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
"@eslint/eslintrc": "^2.1.2",
|
||||
"@eslint/js": "8.48.0",
|
||||
"@humanwhocodes/config-array": "^0.11.10",
|
||||
"@eslint/js": "8.49.0",
|
||||
"@humanwhocodes/config-array": "^0.11.11",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
"ajv": "^6.12.4",
|
||||
@@ -8946,9 +8946,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-8.0.1.tgz",
|
||||
"integrity": "sha512-eEbeEb/mJwh+sNLEhHOWtxMgjN/NEwZUBs1nkiIH2sTQTq07KmPMQ48ihyvo5+Ya56spVOPhunfGr6406crCVA==",
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-9.0.3.tgz",
|
||||
"integrity": "sha512-pI/k4nzBG1PEq1J3XFEHxVvjicfjl8rgaMaqclouGSMPhk7Q3Ejb2ZRxx/ZQOcQ1909HzVoWCFYq6oLgtL4BpQ==",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
@@ -13124,9 +13124,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.0.tgz",
|
||||
"integrity": "sha512-WR0RJE9Ehsio6U4TuM+LmunEsjQ5ncHlw4sn9ihD6RoJKZrVyH9FWV3dmnwu8B2aNib1OvG2X6adUCyFpQyWcg==",
|
||||
"version": "8.14.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
|
||||
"integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@@ -13572,15 +13572,15 @@
|
||||
}
|
||||
},
|
||||
"@eslint/js": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz",
|
||||
"integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==",
|
||||
"version": "8.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz",
|
||||
"integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==",
|
||||
"dev": true
|
||||
},
|
||||
"@excalidraw/excalidraw": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.15.3.tgz",
|
||||
"integrity": "sha512-/gpY7fgMO/AEaFLWnPqzbY8H7ly+/zocFf7D0Is5sWNMD2mhult5tana12lXKLSJ6EAz7ubo1A7LajXzvJXJDA==",
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.16.1.tgz",
|
||||
"integrity": "sha512-4zirHk7dNx6SVq2jQmYOLliqAa1h3WPVqHM5qtJyhD769VsOqwlkopAcnZMb3G1PeIMm6cf2F31quS5MVqvoOQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"@gar/promisify": {
|
||||
@@ -13590,9 +13590,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@humanwhocodes/config-array": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
|
||||
"integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
|
||||
"version": "0.11.11",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
|
||||
"integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@humanwhocodes/object-schema": "^1.2.1",
|
||||
@@ -16215,9 +16215,9 @@
|
||||
}
|
||||
},
|
||||
"dayjs": {
|
||||
"version": "1.11.9",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
|
||||
"integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA=="
|
||||
"version": "1.11.10",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
|
||||
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
|
||||
},
|
||||
"dayjs-plugin-utc": {
|
||||
"version": "0.1.2",
|
||||
@@ -16542,9 +16542,9 @@
|
||||
}
|
||||
},
|
||||
"electron": {
|
||||
"version": "25.8.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-25.8.0.tgz",
|
||||
"integrity": "sha512-T3kC1a/3ntSaYMCVVfUUc9v7myPzi6J2GP0Ad/CyfWKDPp054dGyKxb2EEjKnxQQ7wfjsT1JTEdBG04x6ekVBw==",
|
||||
"version": "25.8.1",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-25.8.1.tgz",
|
||||
"integrity": "sha512-GtcP1nMrROZfFg0+mhyj1hamrHvukfF6of2B/pcWxmWkd5FVY1NJib0tlhiorFZRzQN5Z+APLPr7aMolt7i2AQ==",
|
||||
"requires": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^18.11.18",
|
||||
@@ -17103,16 +17103,16 @@
|
||||
"dev": true
|
||||
},
|
||||
"eslint": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz",
|
||||
"integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==",
|
||||
"version": "8.49.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz",
|
||||
"integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
"@eslint/eslintrc": "^2.1.2",
|
||||
"@eslint/js": "8.48.0",
|
||||
"@humanwhocodes/config-array": "^0.11.10",
|
||||
"@eslint/js": "8.49.0",
|
||||
"@humanwhocodes/config-array": "^0.11.11",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
"ajv": "^6.12.4",
|
||||
@@ -19990,9 +19990,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"marked": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-8.0.1.tgz",
|
||||
"integrity": "sha512-eEbeEb/mJwh+sNLEhHOWtxMgjN/NEwZUBs1nkiIH2sTQTq07KmPMQ48ihyvo5+Ya56spVOPhunfGr6406crCVA=="
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-9.0.3.tgz",
|
||||
"integrity": "sha512-pI/k4nzBG1PEq1J3XFEHxVvjicfjl8rgaMaqclouGSMPhk7Q3Ejb2ZRxx/ZQOcQ1909HzVoWCFYq6oLgtL4BpQ=="
|
||||
},
|
||||
"matcher": {
|
||||
"version": "3.0.0",
|
||||
@@ -23120,9 +23120,9 @@
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.0.tgz",
|
||||
"integrity": "sha512-WR0RJE9Ehsio6U4TuM+LmunEsjQ5ncHlw4sn9ihD6RoJKZrVyH9FWV3dmnwu8B2aNib1OvG2X6adUCyFpQyWcg==",
|
||||
"version": "8.14.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
|
||||
"integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
|
||||
"requires": {}
|
||||
},
|
||||
"xhr": {
|
||||
|
||||
14
package.json
14
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.61.6-beta",
|
||||
"version": "0.61.7-beta",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
@@ -33,7 +33,7 @@
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "6.0.4",
|
||||
"@electron/remote": "2.0.11",
|
||||
"@excalidraw/excalidraw": "0.15.3",
|
||||
"@excalidraw/excalidraw": "0.16.1",
|
||||
"archiver": "5.3.1",
|
||||
"async-mutex": "0.4.0",
|
||||
"axios": "1.5.0",
|
||||
@@ -43,7 +43,7 @@
|
||||
"compression": "1.7.4",
|
||||
"cookie-parser": "1.4.6",
|
||||
"csurf": "1.11.0",
|
||||
"dayjs": "1.11.9",
|
||||
"dayjs": "1.11.10",
|
||||
"dayjs-plugin-utc": "0.1.2",
|
||||
"debounce": "1.2.1",
|
||||
"ejs": "3.1.9",
|
||||
@@ -68,7 +68,7 @@
|
||||
"jimp": "0.22.10",
|
||||
"joplin-turndown-plugin-gfm": "1.0.12",
|
||||
"jsdom": "22.1.0",
|
||||
"marked": "8.0.1",
|
||||
"marked": "9.0.3",
|
||||
"mime-types": "2.1.35",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"node-abi": "3.47.0",
|
||||
@@ -91,17 +91,17 @@
|
||||
"tmp": "0.2.1",
|
||||
"turndown": "7.1.2",
|
||||
"unescape": "1.0.1",
|
||||
"ws": "8.14.0",
|
||||
"ws": "8.14.2",
|
||||
"xml2js": "0.6.2",
|
||||
"yauzl": "2.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "25.8.0",
|
||||
"electron": "25.8.1",
|
||||
"electron-builder": "24.6.4",
|
||||
"electron-packager": "17.1.2",
|
||||
"electron-rebuild": "3.2.9",
|
||||
"eslint": "8.48.0",
|
||||
"eslint": "8.49.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "9.0.0",
|
||||
"eslint-plugin-import": "2.28.1",
|
||||
|
||||
@@ -98,7 +98,12 @@ class BAttachment extends AbstractBeccaEntity {
|
||||
}
|
||||
|
||||
decrypt() {
|
||||
if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
|
||||
if (!this.isProtected || !this.attachmentId) {
|
||||
this.isDecrypted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
|
||||
try {
|
||||
this.title = protectedSessionService.decryptString(this.title);
|
||||
this.isDecrypted = true;
|
||||
|
||||
@@ -211,7 +211,9 @@ class BNote extends AbstractBeccaEntity {
|
||||
return this._getContent();
|
||||
}
|
||||
|
||||
/** @returns {*} */
|
||||
/**
|
||||
* @returns {*}
|
||||
* @throws Error in case of invalid JSON */
|
||||
getJsonContent() {
|
||||
const content = this.getContent();
|
||||
|
||||
@@ -222,6 +224,16 @@ class BNote extends AbstractBeccaEntity {
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
/** @returns {*|null} valid object or null if the content cannot be parsed as JSON */
|
||||
getJsonContentSafely() {
|
||||
try {
|
||||
return this.getJsonContent();
|
||||
}
|
||||
catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param content
|
||||
* @param {object} [opts]
|
||||
@@ -1125,7 +1137,7 @@ class BNote extends AbstractBeccaEntity {
|
||||
}
|
||||
|
||||
/** @returns {BAttachment[]} */
|
||||
getAttachmentByRole(role) {
|
||||
getAttachmentsByRole(role) {
|
||||
return sql.getRows(`
|
||||
SELECT attachments.*
|
||||
FROM attachments
|
||||
@@ -1136,6 +1148,18 @@ class BNote extends AbstractBeccaEntity {
|
||||
.map(row => new BAttachment(row));
|
||||
}
|
||||
|
||||
/** @returns {BAttachment} */
|
||||
getAttachmentByTitle(title) {
|
||||
return sql.getRows(`
|
||||
SELECT attachments.*
|
||||
FROM attachments
|
||||
WHERE ownerId = ?
|
||||
AND title = ?
|
||||
AND isDeleted = 0
|
||||
ORDER BY position`, [this.noteId, title])
|
||||
.map(row => new BAttachment(row))[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles)
|
||||
*
|
||||
@@ -1600,10 +1624,10 @@ class BNote extends AbstractBeccaEntity {
|
||||
noteContent = noteContent.replaceAll(new RegExp(`href="[^"]*attachmentId=${noteAttachment.attachmentId}[^"]*"`, 'gi'),
|
||||
`href="api/attachments/${revisionAttachment.attachmentId}/download"`);
|
||||
}
|
||||
|
||||
revision.setContent(noteContent, {forceSave: true});
|
||||
}
|
||||
|
||||
revision.setContent(noteContent);
|
||||
|
||||
return revision;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -647,4 +647,12 @@ export default class TabManager extends Component {
|
||||
await this.updateDocumentTitle(activeContext);
|
||||
}
|
||||
}
|
||||
|
||||
async frocaReloadedEvent() {
|
||||
const activeContext = this.getActiveContext();
|
||||
|
||||
if (activeContext) {
|
||||
await this.updateDocumentTitle(activeContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,25 @@ export default class FBlob {
|
||||
/** @type {string} */
|
||||
this.utcDateModified = row.utcDateModified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {*}
|
||||
* @throws Error in case of invalid JSON */
|
||||
getJsonContent() {
|
||||
if (!this.content || !this.content.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(this.content);
|
||||
}
|
||||
|
||||
/** @returns {*|null} valid object or null if the content cannot be parsed as JSON */
|
||||
getJsonContentSafely() {
|
||||
try {
|
||||
return this.getJsonContent();
|
||||
}
|
||||
catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +236,12 @@ class FNote {
|
||||
return this.attachments;
|
||||
}
|
||||
|
||||
/** @returns {Promise<FAttachment[]>} */
|
||||
async getAttachmentsByRole(role) {
|
||||
return (await this.getAttachments())
|
||||
.filter(attachment => attachment.role === role);
|
||||
}
|
||||
|
||||
/** @returns {Promise<FAttachment>} */
|
||||
async getAttachmentById(attachmentId) {
|
||||
const attachments = await this.getAttachments();
|
||||
|
||||
@@ -69,7 +69,8 @@ export default class TreeContextMenu {
|
||||
{ title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
|
||||
{ title: 'Sort by ... <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
|
||||
{ title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes },
|
||||
{ title: 'Convert to attachment', command: "convertNoteToAttachment", uiIcon: "bx bx-empty", enabled: isNotRoot && !isHoisted }
|
||||
{ title: 'Convert to attachment', command: "convertNoteToAttachment", uiIcon: "bx bx-empty", enabled: isNotRoot && !isHoisted },
|
||||
{ title: 'Copy note path to clipboard', command: "copyNotePathToClipboard", uiIcon: "bx bx-empty", enabled: true }
|
||||
] },
|
||||
{ title: "----" },
|
||||
{ title: "Protect subtree", command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes },
|
||||
@@ -153,6 +154,9 @@ export default class TreeContextMenu {
|
||||
|
||||
toastService.showMessage(`${converted} notes have been converted to attachments.`);
|
||||
}
|
||||
else if (command === 'copyNotePathToClipboard') {
|
||||
navigator.clipboard.writeText('#' + notePath);
|
||||
}
|
||||
else {
|
||||
this.treeWidget.triggerCommand(command, {
|
||||
node: this.node,
|
||||
|
||||
@@ -33,7 +33,7 @@ async function getRenderedContent(entity, options = {}) {
|
||||
else if (type === 'code') {
|
||||
await renderCode(entity, $renderedContent);
|
||||
}
|
||||
else if (type === 'image') {
|
||||
else if (type === 'image' || type === 'canvas') {
|
||||
renderImage(entity, $renderedContent, options);
|
||||
}
|
||||
else if (!options.tooltip && ['file', 'pdf', 'audio', 'video'].includes(type)) {
|
||||
@@ -49,9 +49,6 @@ async function getRenderedContent(entity, options = {}) {
|
||||
|
||||
$renderedContent.append($content);
|
||||
}
|
||||
else if (type === 'canvas') {
|
||||
await renderCanvas(entity, $renderedContent);
|
||||
}
|
||||
else if (!options.tooltip && type === 'protectedSession') {
|
||||
const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`)
|
||||
.on('click', protectedSessionService.enterProtectedSession);
|
||||
@@ -125,7 +122,7 @@ function renderImage(entity, $renderedContent, options = {}) {
|
||||
let url;
|
||||
|
||||
if (entity instanceof FNote) {
|
||||
url = `api/images/${entity.noteId}/${sanitizedTitle}?${entity.utcDateModified}`;
|
||||
url = `api/images/${entity.noteId}/${sanitizedTitle}?${Math.random()}`;
|
||||
} else if (entity instanceof FAttachment) {
|
||||
url = `api/attachments/${entity.attachmentId}/image/${sanitizedTitle}?${entity.utcDateModified}">`;
|
||||
}
|
||||
@@ -236,28 +233,6 @@ async function renderMermaid(note, $renderedContent) {
|
||||
}
|
||||
}
|
||||
|
||||
async function renderCanvas(note, $renderedContent) {
|
||||
// make sure surrounding container has size of what is visible. Then image is shrinked to its boundaries
|
||||
$renderedContent.css({height: "100%", width: "100%"});
|
||||
|
||||
const blob = await note.getBlob();
|
||||
const content = blob.content || "";
|
||||
|
||||
try {
|
||||
const placeHolderSVG = "<svg />";
|
||||
const data = JSON.parse(content)
|
||||
const svg = data.svg || placeHolderSVG;
|
||||
/**
|
||||
* maxWidth: size down to 100% (full) width of container but do not enlarge!
|
||||
* height:auto to ensure that height scales with width
|
||||
*/
|
||||
$renderedContent.append($(svg).css({maxWidth: "100%", maxHeight: "100%", height: "auto", width: "auto"}));
|
||||
} catch (err) {
|
||||
console.error("error parsing content as JSON", content, err);
|
||||
$renderedContent.append($("<div>").text("Error parsing content. Please check console.error() for more details."));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {jQuery} $renderedContent
|
||||
* @param {FNote} note
|
||||
|
||||
@@ -194,6 +194,10 @@ function goToLink(evt) {
|
||||
const $link = $(evt.target).closest("a,.block-link");
|
||||
const hrefLink = $link.attr('href') || $link.attr('data-href');
|
||||
|
||||
return goToLinkExt(evt, hrefLink, $link);
|
||||
}
|
||||
|
||||
function goToLinkExt(evt, hrefLink, $link) {
|
||||
if (hrefLink?.startsWith("data:")) {
|
||||
return true;
|
||||
}
|
||||
@@ -201,7 +205,7 @@ function goToLink(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink);
|
||||
const {notePath, viewScope} = parseNavigationStateFromUrl(hrefLink);
|
||||
|
||||
const ctrlKey = utils.isCtrlKey(evt);
|
||||
const isLeftClick = evt.which === 1;
|
||||
@@ -213,25 +217,23 @@ function goToLink(evt) {
|
||||
|
||||
if (notePath) {
|
||||
if (openInNewTab) {
|
||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath, { viewScope });
|
||||
}
|
||||
else if (isLeftClick) {
|
||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath, {viewScope});
|
||||
} else if (isLeftClick) {
|
||||
const ntxId = $(evt.target).closest("[data-ntx-id]").attr("data-ntx-id");
|
||||
|
||||
const noteContext = ntxId
|
||||
? appContext.tabManager.getNoteContextById(ntxId)
|
||||
: appContext.tabManager.getActiveContext();
|
||||
|
||||
noteContext.setNote(notePath, { viewScope }).then(() => {
|
||||
noteContext.setNote(notePath, {viewScope}).then(() => {
|
||||
if (noteContext !== appContext.tabManager.getActiveContext()) {
|
||||
appContext.tabManager.activateNoteContext(noteContext.ntxId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (hrefLink) {
|
||||
const withinEditLink = $link.hasClass("ck-link-actions__preview");
|
||||
const outsideOfCKEditor = $link.closest("[contenteditable]").length === 0;
|
||||
} else if (hrefLink) {
|
||||
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
|
||||
const outsideOfCKEditor = !$link || $link.closest("[contenteditable]").length === 0;
|
||||
|
||||
if (openInNewTab
|
||||
|| (withinEditLink && (leftClick || middleClick))
|
||||
@@ -239,8 +241,7 @@ function goToLink(evt) {
|
||||
) {
|
||||
if (hrefLink.toLowerCase().startsWith('http') || hrefLink.startsWith("api/")) {
|
||||
window.open(hrefLink, '_blank');
|
||||
}
|
||||
else if (hrefLink.toLowerCase().startsWith('file:') && utils.isElectron()) {
|
||||
} else if (hrefLink.toLowerCase().startsWith('file:') && utils.isElectron()) {
|
||||
const electron = utils.dynamicRequire('electron');
|
||||
|
||||
electron.shell.openPath(hrefLink);
|
||||
@@ -364,6 +365,7 @@ export default {
|
||||
getNotePathFromUrl,
|
||||
createLink,
|
||||
goToLink,
|
||||
goToLinkExt,
|
||||
loadReferenceLinkTitle,
|
||||
getReferenceLinkTitle,
|
||||
getReferenceLinkTitleSync,
|
||||
|
||||
@@ -55,6 +55,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$widget.on('show.bs.dropdown', () => this.refreshVisibility(this.note));
|
||||
|
||||
this.$convertNoteIntoAttachmentButton = this.$widget.find("[data-trigger-command='convertNoteIntoAttachment']");
|
||||
this.$findInTextButton = this.$widget.find('.find-in-text-button');
|
||||
@@ -92,22 +93,22 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
|
||||
});
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
async refreshVisibility(note) {
|
||||
this.$convertNoteIntoAttachmentButton.toggle(note.isEligibleForConversionToAttachment());
|
||||
|
||||
this.toggleDisabled(this.$findInTextButton, ['text', 'code', 'book'].includes(note.type));
|
||||
|
||||
this.toggleDisabled(this.$showSourceButton, ['text', 'relationMap', 'mermaid'].includes(note.type));
|
||||
this.toggleDisabled(this.$showSourceButton, ['text', 'code', 'relationMap', 'mermaid', 'canvas'].includes(note.type));
|
||||
|
||||
this.toggleDisabled(this.$printActiveNoteButton, ['text', 'code'].includes(note.type));
|
||||
|
||||
this.$renderNoteButton.toggle(note.type === 'render');
|
||||
|
||||
this.toggleDisabled(this.$openNoteExternallyButton, utils.isElectron() && !['search'].includes(note.type));
|
||||
this.toggleDisabled(this.$openNoteExternallyButton, utils.isElectron() && !['search', 'book'].includes(note.type));
|
||||
this.toggleDisabled(this.$openNoteCustomButton,
|
||||
utils.isElectron()
|
||||
&& !utils.isMac() // no implementation for Mac yet
|
||||
&& !['search'].includes(note.type)
|
||||
&& !['search', 'book'].includes(note.type)
|
||||
);
|
||||
|
||||
// I don't want to handle all special notes like this, but intuitively user might want to export content of backend log
|
||||
|
||||
@@ -86,6 +86,8 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
protectedSessionHolder.touchProtectedSessionIfNecessary(note);
|
||||
|
||||
await server.put(`notes/${noteId}/data`, data, this.componentId);
|
||||
|
||||
this.getTypeWidget().dataSaved?.();
|
||||
});
|
||||
|
||||
appContext.addBeforeUnloadListener(this);
|
||||
@@ -167,7 +169,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
let type = note.type;
|
||||
const viewScope = this.noteContext.viewScope;
|
||||
|
||||
if (type === 'text' && viewScope.viewMode === 'source') {
|
||||
if (viewScope.viewMode === 'source') {
|
||||
type = 'readOnlyCode';
|
||||
} else if (viewScope.viewMode === 'attachments') {
|
||||
type = viewScope.attachmentId ? 'attachmentDetail' : 'attachmentList';
|
||||
|
||||
@@ -14,7 +14,6 @@ import keyboardActionsService from "../services/keyboard_actions.js";
|
||||
import clipboard from "../services/clipboard.js";
|
||||
import protectedSessionService from "../services/protected_session.js";
|
||||
import linkService from "../services/link.js";
|
||||
import syncService from "../services/sync.js";
|
||||
import options from "../services/options.js";
|
||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||
import dialogService from "../services/dialog.js";
|
||||
@@ -586,6 +585,17 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
});
|
||||
},
|
||||
select: (event, {node}) => {
|
||||
if (hoistedNoteService.getHoistedNoteId() === 'root'
|
||||
&& node.data.noteId === '_hidden'
|
||||
&& node.isSelected()) {
|
||||
|
||||
// hidden is hackily hidden from the tree via CSS when root is hoisted
|
||||
// make sure it's not selected by mistake, it could be e.g. deleted by mistake otherwise
|
||||
node.setSelected(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$(node.span).find(".fancytree-custom-icon").attr("title",
|
||||
node.isSelected() ? "Apply bulk actions on selected notes" : "");
|
||||
}
|
||||
@@ -799,7 +809,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
nodes.push(this.getActiveNode());
|
||||
}
|
||||
|
||||
return nodes;
|
||||
// hidden subtree is hackily hidden via CSS when hoisted to root
|
||||
// make sure it's never selected for e.g. deletion in such a case
|
||||
return nodes.filter(node => hoistedNoteService.getHoistedNoteId() !== 'root'
|
||||
|| node.data.noteId !== '_hidden');
|
||||
}
|
||||
|
||||
async setExpandedStatusForSubtree(node, isExpanded) {
|
||||
|
||||
@@ -42,10 +42,12 @@ export default class AttachmentListTypeWidget extends TypeWidget {
|
||||
const $helpButton = $('<button class="attachment-help-button" type="button" data-help-page="attachments" title="Open help page on attachments"><span class="bx bx-help-circle"></span></button>');
|
||||
utils.initHelpButtons($helpButton);
|
||||
|
||||
const noteLink = await linkService.createLink(this.noteId); // do separately to avoid race condition between empty() and .append()
|
||||
|
||||
this.$linksWrapper.empty().append(
|
||||
$('<div>').append(
|
||||
"Owning note: ",
|
||||
await linkService.createLink(this.noteId),
|
||||
noteLink,
|
||||
),
|
||||
$('<div>').append(
|
||||
$('<button class="btn btn-sm">')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import libraryLoader from "../../services/library_loader.js";
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import utils from '../../services/utils.js';
|
||||
import linkService from '../../services/link.js';
|
||||
import debounce from "../../services/debounce.js";
|
||||
|
||||
const {sleep} = utils;
|
||||
@@ -19,10 +20,13 @@ const TPL = `
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.excalidraw-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.excalidraw button[data-testid="json-export-button"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:root[dir="ltr"]
|
||||
.excalidraw
|
||||
@@ -51,8 +55,8 @@ const TPL = `
|
||||
* for sketching. Excalidraw has a vibrant and active community.
|
||||
*
|
||||
* Functionality:
|
||||
* We store the excalidraw assets (elements, appState, files) in the note. In addition to that, we
|
||||
* export the SVG from the canvas on every update. The SVG is also saved in the note. It is used when
|
||||
* We store the excalidraw assets (elements and files) in the note. In addition to that, we
|
||||
* export the SVG from the canvas on every update and store it in the note's attachment. It is used when
|
||||
* calling api/images and makes referencing very easy.
|
||||
*
|
||||
* Paths not taken.
|
||||
@@ -75,7 +79,7 @@ const TPL = `
|
||||
* - the 3 excalidraw fonts should be included in the share and everywhere, so that it is shown
|
||||
* when requiring svg.
|
||||
*
|
||||
* Discussion of storing svg in the note:
|
||||
* Discussion of storing svg in the note attachment:
|
||||
* - Pro: we will combat bit-rot. Showing the SVG will be very fast and easy, since it is already there.
|
||||
* - Con: The note will get bigger (~40-50%?), we will generate more bandwidth. However, using trilium
|
||||
* desktop instance mitigates that issue.
|
||||
@@ -84,7 +88,6 @@ const TPL = `
|
||||
* - Support image-notes as reference in excalidraw
|
||||
* - Support canvas note as reference (svg) in other canvas notes.
|
||||
* - Make it easy to include a canvas note inside a text note
|
||||
* - Support for excalidraw libraries. Maybe special code notes with a tag.
|
||||
*/
|
||||
export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
constructor() {
|
||||
@@ -113,6 +116,8 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
this.createExcalidrawReactApp = this.createExcalidrawReactApp.bind(this);
|
||||
this.onChangeHandler = this.onChangeHandler.bind(this);
|
||||
this.isNewSceneVersion = this.isNewSceneVersion.bind(this);
|
||||
|
||||
this.libraryChanged = false;
|
||||
}
|
||||
|
||||
static getType() {
|
||||
@@ -129,7 +134,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
}
|
||||
});
|
||||
|
||||
this.$widget.toggleClass("full-height", true); // only add
|
||||
this.$widget.toggleClass("full-height", true);
|
||||
this.$render = this.$widget.find('.canvas-render');
|
||||
const documentStyle = window.getComputedStyle(document.documentElement);
|
||||
this.themeStyle = documentStyle.getPropertyValue('--theme-style')?.trim();
|
||||
@@ -166,54 +171,47 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
const blob = await note.getBlob();
|
||||
|
||||
// before we load content into excalidraw, make sure excalidraw has loaded
|
||||
while (!this.excalidrawRef || !this.excalidrawRef.current) {
|
||||
while (!this.excalidrawRef?.current) {
|
||||
console.log("excalidrawRef not yet loaded, sleep 200ms...");
|
||||
await sleep(200);
|
||||
}
|
||||
|
||||
const appState = {
|
||||
theme: this.themeStyle
|
||||
};
|
||||
|
||||
/**
|
||||
* new and empty note - make sure that canvas is empty.
|
||||
* If we do not set it manually, we occasionally get some "bleeding" from another
|
||||
* note into this fresh note. Probably due to that this note-instance does not get
|
||||
* newly instantiated?
|
||||
*/
|
||||
if (this.excalidrawRef.current && blob.content?.trim() === "") {
|
||||
if (!blob.content?.trim()) {
|
||||
const sceneData = {
|
||||
elements: [],
|
||||
appState: {
|
||||
theme: this.themeStyle
|
||||
},
|
||||
appState,
|
||||
collaborators: []
|
||||
};
|
||||
|
||||
this.excalidrawRef.current.updateScene(sceneData);
|
||||
}
|
||||
else if (this.excalidrawRef.current && blob.content) {
|
||||
else if (blob.content) {
|
||||
// load saved content into excalidraw canvas
|
||||
let content;
|
||||
|
||||
try {
|
||||
content = JSON.parse(blob.content || "");
|
||||
content = blob.getJsonContent();
|
||||
} catch(err) {
|
||||
console.error("Error parsing content. Probably note.type changed",
|
||||
"Starting with empty canvas"
|
||||
, note, blob, err);
|
||||
console.error("Error parsing content. Probably note.type changed. Starting with empty canvas", note, blob, err);
|
||||
|
||||
content = {
|
||||
elements: [],
|
||||
appState: {},
|
||||
files: [],
|
||||
};
|
||||
}
|
||||
|
||||
const {elements, appState, files} = content;
|
||||
const {elements, files} = content;
|
||||
|
||||
appState.theme = this.themeStyle;
|
||||
|
||||
/**
|
||||
* use widths and offsets of current view, since stored appState has the state from
|
||||
* previous edit. using the stored state would lead to pointer mismatch.
|
||||
*/
|
||||
const boundingClientRect = this.excalidrawWrapperRef.current.getBoundingClientRect();
|
||||
appState.width = boundingClientRect.width;
|
||||
appState.height = boundingClientRect.height;
|
||||
@@ -243,6 +241,19 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
this.excalidrawRef.current.addFiles(fileArray);
|
||||
}
|
||||
|
||||
Promise.all(
|
||||
(await note.getAttachmentsByRole('canvasLibraryItem'))
|
||||
.map(attachment => attachment.getBlob())
|
||||
).then(blobs => {
|
||||
if (note.noteId !== this.currentNoteId) {
|
||||
// current note changed in the course of the async operation
|
||||
return;
|
||||
}
|
||||
|
||||
const libraryItems = blobs.map(blob => blob.getJsonContentSafely()).filter(item => !!item);
|
||||
this.excalidrawRef.current.updateLibrary({libraryItems, merge: false});
|
||||
});
|
||||
|
||||
// set initial scene version
|
||||
if (this.currentSceneVersion === this.SCENE_VERSION_INITIAL) {
|
||||
this.currentSceneVersion = this.getSceneVersion();
|
||||
@@ -263,10 +274,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
*/
|
||||
const files = this.excalidrawRef.current.getFiles();
|
||||
|
||||
/**
|
||||
* parallel svg export to combat bitrot and enable rendering image for note inclusion,
|
||||
* preview, and share.
|
||||
*/
|
||||
// parallel svg export to combat bitrot and enable rendering image for note inclusion, preview, and share
|
||||
const svg = await window.ExcalidrawLib.exportToSvg({
|
||||
elements,
|
||||
appState,
|
||||
@@ -286,15 +294,38 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
const content = {
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
_meta: "This note has type `canvas`. It uses excalidraw and stores an exported svg alongside.",
|
||||
elements, // excalidraw
|
||||
appState, // excalidraw
|
||||
files: activeFiles, // excalidraw
|
||||
svg: svgString, // not needed for excalidraw, used for note_short, content, and image api
|
||||
elements,
|
||||
files: activeFiles
|
||||
};
|
||||
|
||||
const attachments = [
|
||||
{ role: 'image', title: 'canvas-export.svg', mime: 'image/svg+xml', content: svgString, position: 0 }
|
||||
];
|
||||
|
||||
if (this.libraryChanged) {
|
||||
// this.libraryChanged is unset in dataSaved()
|
||||
|
||||
// there's no separate method to get library items, so have to abuse this one
|
||||
const libraryItems = await this.excalidrawRef.current.updateLibrary({merge: true});
|
||||
|
||||
let position = 10;
|
||||
|
||||
for (const libraryItem of libraryItems) {
|
||||
attachments.push({
|
||||
role: 'canvasLibraryItem',
|
||||
title: libraryItem.id,
|
||||
mime: 'application/json',
|
||||
content: JSON.stringify(libraryItem),
|
||||
position: position
|
||||
});
|
||||
|
||||
position += 10;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: JSON.stringify(content)
|
||||
content: JSON.stringify(content),
|
||||
attachments: attachments
|
||||
};
|
||||
}
|
||||
|
||||
@@ -306,6 +337,10 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
this.spacedUpdate.scheduleUpdate();
|
||||
}
|
||||
|
||||
dataSaved() {
|
||||
this.libraryChanged = false;
|
||||
}
|
||||
|
||||
onChangeHandler() {
|
||||
// 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.
|
||||
@@ -323,8 +358,6 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
if (shouldSave) {
|
||||
this.updateSceneVersion();
|
||||
this.saveData();
|
||||
} else {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,21 +399,17 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
}, [excalidrawWrapperRef]);
|
||||
|
||||
const onLinkOpen = React.useCallback((element, event) => {
|
||||
const link = element.link;
|
||||
const { nativeEvent } = event.detail;
|
||||
const isNewTab = nativeEvent.ctrlKey || nativeEvent.metaKey;
|
||||
const isNewWindow = nativeEvent.shiftKey;
|
||||
const isInternalLink = link.startsWith("/")
|
||||
|| link.includes(window.location.origin);
|
||||
let link = element.link;
|
||||
|
||||
if (isInternalLink && !isNewTab && !isNewWindow) {
|
||||
// signal that we're handling the redirect ourselves
|
||||
event.preventDefault();
|
||||
// do a custom redirect, such as passing to react-router
|
||||
// ...
|
||||
} else {
|
||||
// open in the same tab
|
||||
if (link.startsWith("root/")) {
|
||||
link = "#" + link;
|
||||
}
|
||||
|
||||
const { nativeEvent } = event.detail;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
return linkService.goToLinkExt(nativeEvent, link, null);
|
||||
}, []);
|
||||
|
||||
return React.createElement(
|
||||
@@ -401,6 +430,11 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
onPaste: (data, event) => {
|
||||
console.log("Verbose: excalidraw internal paste. No trilium action implemented.", data, event);
|
||||
},
|
||||
onLibraryChange: () => {
|
||||
this.libraryChanged = true;
|
||||
|
||||
this.saveData();
|
||||
},
|
||||
onChange: debounce(this.onChangeHandler, this.DEBOUNCE_TIME_ONCHANGEHANDLER),
|
||||
viewModeEnabled: false,
|
||||
zenModeEnabled: false,
|
||||
@@ -408,15 +442,19 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
isCollaborating: false,
|
||||
detectScroll: false,
|
||||
handleKeyboardGlobally: false,
|
||||
autoFocus: true,
|
||||
autoFocus: false,
|
||||
onLinkOpen,
|
||||
UIOptions: {
|
||||
saveToActiveFile: false,
|
||||
saveAsImage: false
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* needed to ensure, that multipleOnChangeHandler calls do not trigger a safe.
|
||||
* 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
|
||||
*
|
||||
@@ -426,8 +464,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
const sceneVersion = this.getSceneVersion();
|
||||
|
||||
return this.currentSceneVersion === this.SCENE_VERSION_INITIAL // initial scene version update
|
||||
|| this.currentSceneVersion !== sceneVersion // ensure scene changed
|
||||
;
|
||||
|| this.currentSceneVersion !== sceneVersion; // ensure scene changed
|
||||
}
|
||||
|
||||
getSceneVersion() {
|
||||
|
||||
@@ -21,19 +21,24 @@ function returnImage(req, res) {
|
||||
* to avoid bitrot and enable usage as referenced image the svg is included.
|
||||
*/
|
||||
if (image.type === 'canvas') {
|
||||
const content = image.getContent();
|
||||
try {
|
||||
const data = JSON.parse(content);
|
||||
let svgString = '<svg/>'
|
||||
const attachment = image.getAttachmentByTitle('canvas-export.svg');
|
||||
|
||||
const svg = data.svg || '<svg />'
|
||||
res.set('Content-Type', "image/svg+xml");
|
||||
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.send(svg);
|
||||
} catch(err) {
|
||||
res.setHeader("Content-Type", "text/plain")
|
||||
.status(500)
|
||||
.send("there was an error parsing excalidraw to svg");
|
||||
if (attachment) {
|
||||
svgString = attachment.getContent();
|
||||
} else {
|
||||
// backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key
|
||||
const contentSvg = image.getJsonContentSafely()?.svg;
|
||||
|
||||
if (contentSvg) {
|
||||
svgString = contentSvg;
|
||||
}
|
||||
}
|
||||
|
||||
const svg = svgString
|
||||
res.set('Content-Type', "image/svg+xml");
|
||||
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.send(svg);
|
||||
} else {
|
||||
res.set('Content-Type', image.mime);
|
||||
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
@@ -50,7 +55,9 @@ function returnAttachedImage(req, res) {
|
||||
}
|
||||
|
||||
if (!["image"].includes(attachment.role)) {
|
||||
return res.sendStatus(400);
|
||||
return res.setHeader("Content-Type", "text/plain")
|
||||
.status(400)
|
||||
.send(`Attachment '${attachment.attachmentId}' has role '${attachment.role}', but 'image' was expected.`);
|
||||
}
|
||||
|
||||
res.set('Content-Type', attachment.mime);
|
||||
|
||||
@@ -45,10 +45,10 @@ function createNote(req) {
|
||||
}
|
||||
|
||||
function updateNoteData(req) {
|
||||
const {content} = req.body;
|
||||
const {content, attachments} = req.body;
|
||||
const {noteId} = req.params;
|
||||
|
||||
return noteService.updateNoteData(noteId, content);
|
||||
return noteService.updateNoteData(noteId, content, attachments);
|
||||
}
|
||||
|
||||
function deleteNote(req) {
|
||||
|
||||
@@ -166,11 +166,7 @@ function update(req) {
|
||||
|
||||
const {entities, instanceId} = body;
|
||||
|
||||
sql.transactional(() => {
|
||||
for (const {entityChange, entity} of entities) {
|
||||
syncUpdateService.updateEntity(entityChange, entity, instanceId);
|
||||
}
|
||||
});
|
||||
sql.transactional(() => syncUpdateService.updateEntities(entities, instanceId));
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2023-09-06T23:57:29+02:00", buildRevision: "6fa9d996e84f87fcb73c3388a5170affd2c2f7cc" };
|
||||
module.exports = { buildDate:"2023-09-21T23:38:18+02:00", buildRevision: "79e5e3b65ff613cdb81e2afaa832037ccf06d7b8" };
|
||||
|
||||
@@ -12,6 +12,7 @@ const BBranch = require('../becca/entities/bbranch');
|
||||
const revisionService = require('./revisions');
|
||||
const becca = require("../becca/becca");
|
||||
const utils = require("../services/utils");
|
||||
const eraseService = require("../services/erase");
|
||||
const {sanitizeAttributeName} = require("./sanitize_attribute_name");
|
||||
const noteTypes = require("../services/note_types").getNoteTypeNames();
|
||||
|
||||
@@ -440,7 +441,7 @@ class ConsistencyChecks {
|
||||
this.findAndFixIssues(`
|
||||
SELECT notes.noteId, notes.type, notes.mime
|
||||
FROM notes
|
||||
JOIN blobs USING (blobId)
|
||||
JOIN blobs USING (blobId)
|
||||
WHERE isDeleted = 0
|
||||
AND isProtected = 0
|
||||
AND content IS NULL`,
|
||||
@@ -460,19 +461,36 @@ class ConsistencyChecks {
|
||||
}
|
||||
|
||||
this.findAndFixIssues(`
|
||||
SELECT revisions.revisionId
|
||||
SELECT revisions.revisionId, blobs.blobId
|
||||
FROM revisions
|
||||
LEFT JOIN blobs USING (blobId)
|
||||
WHERE blobs.blobId IS NULL`,
|
||||
({revisionId}) => {
|
||||
({revisionId, blobId}) => {
|
||||
if (this.autoFix) {
|
||||
revisionService.eraseRevisions([revisionId]);
|
||||
|
||||
this.reloadNeeded = true;
|
||||
|
||||
logFix(`Note revision content '${revisionId}' was set to erased since its content did not exist.`);
|
||||
logFix(`Note revision '${revisionId}' was erased since the referenced blob '${blobId}' did not exist.`);
|
||||
} else {
|
||||
logError(`Note revision content '${revisionId}' does not exist`);
|
||||
logError(`Note revision '${revisionId}' blob '${blobId}' does not exist`);
|
||||
}
|
||||
});
|
||||
|
||||
this.findAndFixIssues(`
|
||||
SELECT attachments.attachmentId, blobs.blobId
|
||||
FROM attachments
|
||||
LEFT JOIN blobs USING (blobId)
|
||||
WHERE blobs.blobId IS NULL`,
|
||||
({attachmentId, blobId}) => {
|
||||
if (this.autoFix) {
|
||||
eraseService.eraseAttachments([attachmentId]);
|
||||
|
||||
this.reloadNeeded = true;
|
||||
|
||||
logFix(`Attachment '${attachmentId}' was erased since the referenced blob '${blobId}' did not exist.`);
|
||||
} else {
|
||||
logError(`Attachment '${attachmentId}' blob '${blobId}' does not exist`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -183,5 +183,6 @@ module.exports = {
|
||||
eraseDeletedNotesNow,
|
||||
eraseUnusedAttachmentsNow,
|
||||
eraseNotesWithDeleteId,
|
||||
eraseUnusedBlobs
|
||||
eraseUnusedBlobs,
|
||||
eraseAttachments
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ const becca = require('../becca/becca');
|
||||
const BBranch = require('../becca/entities/bbranch');
|
||||
const BNote = require('../becca/entities/bnote');
|
||||
const BAttribute = require('../becca/entities/battribute');
|
||||
const BAttachment = require("../becca/entities/battachment");
|
||||
const dayjs = require("dayjs");
|
||||
const htmlSanitizer = require("./html_sanitizer");
|
||||
const ValidationError = require("../errors/validation_error");
|
||||
@@ -732,7 +733,7 @@ function saveRevisionIfNeeded(note) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateNoteData(noteId, content) {
|
||||
function updateNoteData(noteId, content, attachments = []) {
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
if (!note.isContentAvailable()) {
|
||||
@@ -744,6 +745,23 @@ function updateNoteData(noteId, content) {
|
||||
const { forceFrontendReload, content: newContent } = saveLinks(note, content);
|
||||
|
||||
note.setContent(newContent, { forceFrontendReload });
|
||||
|
||||
if (attachments?.length > 0) {
|
||||
/** @var {Object<string, BAttachment>} */
|
||||
const existingAttachmentsByTitle = utils.toMap(note.getAttachments({includeContentLength: false}), 'title');
|
||||
|
||||
for (const {attachmentId, role, mime, title, content, position} of attachments) {
|
||||
if (attachmentId || !(title in existingAttachmentsByTitle)) {
|
||||
note.saveAttachment({attachmentId, role, mime, title, content, position});
|
||||
} else {
|
||||
const existingAttachment = existingAttachmentsByTitle[title];
|
||||
existingAttachment.role = role;
|
||||
existingAttachment.mime = mime;
|
||||
existingAttachment.position = position;
|
||||
existingAttachment.setContent(content, {forceSave: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -810,6 +828,16 @@ function undeleteBranch(branchId, deleteId, taskContext) {
|
||||
new BAttribute(attributeRow).save({skipValidation: true});
|
||||
}
|
||||
|
||||
const attachmentRows = sql.getRows(`
|
||||
SELECT * FROM attachments
|
||||
WHERE isDeleted = 1
|
||||
AND deleteId = ?
|
||||
AND ownerId = ?`, [deleteId, noteRow.noteId]);
|
||||
|
||||
for (const attachmentRow of attachmentRows) {
|
||||
new BAttachment(attachmentRow).save();
|
||||
}
|
||||
|
||||
const childBranchIds = sql.getColumn(`
|
||||
SELECT branches.branchId
|
||||
FROM branches
|
||||
@@ -867,6 +895,10 @@ async function asyncPostProcessContent(note, content) {
|
||||
|
||||
// all keys should be replaced by the corresponding values
|
||||
function replaceByMap(str, mapObj) {
|
||||
if (!mapObj) {
|
||||
return str;
|
||||
}
|
||||
|
||||
const re = new RegExp(Object.keys(mapObj).join("|"),"g");
|
||||
|
||||
return str.replace(re, matched => mapObj[matched]);
|
||||
|
||||
@@ -14,21 +14,34 @@ function protectRevisions(note) {
|
||||
}
|
||||
|
||||
for (const revision of note.getRevisions()) {
|
||||
if (note.isProtected === revision.isProtected) {
|
||||
continue;
|
||||
if (note.isProtected !== revision.isProtected) {
|
||||
try {
|
||||
const content = revision.getContent();
|
||||
|
||||
revision.isProtected = note.isProtected;
|
||||
|
||||
// this will force de/encryption
|
||||
revision.setContent(content, {forceSave: true});
|
||||
} catch (e) {
|
||||
log.error(`Could not un/protect note revision '${revision.revisionId}'`);
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const content = revision.getContent();
|
||||
for (const attachment of revision.getAttachments()) {
|
||||
if (note.isProtected !== attachment.isProtected) {
|
||||
try {
|
||||
const content = attachment.getContent();
|
||||
|
||||
revision.isProtected = note.isProtected;
|
||||
attachment.isProtected = note.isProtected;
|
||||
attachment.setContent(content, {forceSave: true});
|
||||
} catch (e) {
|
||||
log.error(`Could not un/protect attachment '${attachment.attachmentId}'`);
|
||||
|
||||
// this will force de/encryption
|
||||
revision.setContent(content, {forceSave: true});
|
||||
} catch (e) {
|
||||
log.error(`Could not un/protect note revision '${revision.revisionId}'`);
|
||||
|
||||
throw e;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +51,7 @@ function eraseRevisions(revisionIdsToErase) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`Removing note revisions: ${JSON.stringify(revisionIdsToErase)}`);
|
||||
log.info(`Removing revisions: ${JSON.stringify(revisionIdsToErase)}`);
|
||||
|
||||
sql.executeMany(`DELETE FROM revisions WHERE revisionId IN (???)`, revisionIdsToErase);
|
||||
sql.executeMany(`UPDATE entity_changes SET isErased = 1, utcDateChanged = '${dateUtils.utcNowDateTime()}' WHERE entityName = 'revisions' AND entityId IN (???)`, revisionIdsToErase);
|
||||
|
||||
@@ -93,7 +93,7 @@ async function setupSyncFromSyncServer(syncServerHost, syncProxy, password) {
|
||||
return { result: 'success' };
|
||||
}
|
||||
catch (e) {
|
||||
log.error(`Sync failed: ${e.message}`);
|
||||
log.error(`Sync failed: '${e.message}', stack: ${e.stack}`);
|
||||
|
||||
return {
|
||||
result: 'failure',
|
||||
|
||||
@@ -26,7 +26,7 @@ const LOG_ALL_QUERIES = false;
|
||||
});
|
||||
|
||||
function insert(tableName, rec, replace = false) {
|
||||
const keys = Object.keys(rec);
|
||||
const keys = Object.keys(rec || {});
|
||||
if (keys.length === 0) {
|
||||
log.error(`Can't insert empty object into table ${tableName}`);
|
||||
return;
|
||||
@@ -53,7 +53,7 @@ function replace(tableName, rec) {
|
||||
}
|
||||
|
||||
function upsert(tableName, primaryKey, rec) {
|
||||
const keys = Object.keys(rec);
|
||||
const keys = Object.keys(rec || {});
|
||||
if (keys.length === 0) {
|
||||
log.error(`Can't upsert empty object into table ${tableName}`);
|
||||
return;
|
||||
|
||||
@@ -71,8 +71,7 @@ async function sync() {
|
||||
};
|
||||
}
|
||||
else {
|
||||
log.info(`sync failed: ${e.message}
|
||||
stack: ${e.stack}`);
|
||||
log.info(`Sync failed: '${e.message}', stack: ${e.stack}`);
|
||||
|
||||
ws.syncFailed();
|
||||
|
||||
@@ -127,8 +126,6 @@ async function doLogin() {
|
||||
}
|
||||
|
||||
async function pullChanges(syncContext) {
|
||||
let atLeastOnePullApplied = false;
|
||||
|
||||
while (true) {
|
||||
const lastSyncedPull = getLastSyncedPull();
|
||||
const logMarkerId = utils.randomString(10); // to easily pair sync events between client and server logs
|
||||
@@ -144,22 +141,7 @@ async function pullChanges(syncContext) {
|
||||
const pulledDate = Date.now();
|
||||
|
||||
sql.transactional(() => {
|
||||
for (const {entityChange, entity} of entityChanges) {
|
||||
const changeAppliedAlready = entityChange.changeId
|
||||
&& !!sql.getValue("SELECT 1 FROM entity_changes WHERE changeId = ?", [entityChange.changeId]);
|
||||
|
||||
if (changeAppliedAlready) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!atLeastOnePullApplied) { // send only for first
|
||||
ws.syncPullInProgress();
|
||||
|
||||
atLeastOnePullApplied = true;
|
||||
}
|
||||
|
||||
syncUpdateService.updateEntity(entityChange, entity, syncContext.instanceId);
|
||||
}
|
||||
syncUpdateService.updateEntities(entityChanges, syncContext.instanceId);
|
||||
|
||||
if (lastSyncedPull !== lastEntityChangeId) {
|
||||
setLastSyncedPull(lastEntityChangeId);
|
||||
|
||||
@@ -3,15 +3,51 @@ const log = require('./log');
|
||||
const entityChangesService = require('./entity_changes');
|
||||
const eventService = require('./events');
|
||||
const entityConstructor = require("../becca/entity_constructor");
|
||||
const ws = require("./ws");
|
||||
|
||||
function updateEntity(remoteEC, remoteEntityRow, instanceId) {
|
||||
function updateEntities(entityChanges, instanceId) {
|
||||
if (entityChanges.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let atLeastOnePullApplied = false;
|
||||
const updateContext = {
|
||||
updated: {},
|
||||
alreadyUpdated: 0,
|
||||
erased: 0,
|
||||
alreadyErased: 0
|
||||
};
|
||||
|
||||
for (const {entityChange, entity} of entityChanges) {
|
||||
const changeAppliedAlready = entityChange.changeId
|
||||
&& !!sql.getValue("SELECT 1 FROM entity_changes WHERE changeId = ?", [entityChange.changeId]);
|
||||
|
||||
if (changeAppliedAlready) {
|
||||
updateContext.alreadyUpdated++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!atLeastOnePullApplied) { // avoid spamming and send only for first
|
||||
ws.syncPullInProgress();
|
||||
|
||||
atLeastOnePullApplied = true;
|
||||
}
|
||||
|
||||
updateEntity(entityChange, entity, instanceId, updateContext);
|
||||
}
|
||||
|
||||
logUpdateContext(updateContext);
|
||||
}
|
||||
|
||||
function updateEntity(remoteEC, remoteEntityRow, instanceId, updateContext) {
|
||||
if (!remoteEntityRow && remoteEC.entityName === 'options') {
|
||||
return; // can be undefined for options with isSynced=false
|
||||
}
|
||||
|
||||
const updated = remoteEC.entityName === 'note_reordering'
|
||||
? updateNoteReordering(remoteEC, remoteEntityRow, instanceId)
|
||||
: updateNormalEntity(remoteEC, remoteEntityRow, instanceId);
|
||||
: updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext);
|
||||
|
||||
if (updated) {
|
||||
if (remoteEntityRow?.isDeleted) {
|
||||
@@ -29,11 +65,12 @@ function updateEntity(remoteEC, remoteEntityRow, instanceId) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateNormalEntity(remoteEC, remoteEntityRow, instanceId) {
|
||||
function updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext) {
|
||||
const localEC = sql.getRow(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [remoteEC.entityName, remoteEC.entityId]);
|
||||
|
||||
if (!localEC?.isErased && remoteEC.isErased) {
|
||||
eraseEntity(remoteEC, instanceId);
|
||||
updateContext.erased++;
|
||||
|
||||
return true;
|
||||
} else if (localEC?.isErased && !remoteEC.isErased) {
|
||||
@@ -42,10 +79,15 @@ function updateNormalEntity(remoteEC, remoteEntityRow, instanceId) {
|
||||
|
||||
return false;
|
||||
} else if (localEC?.isErased && remoteEC.isErased) {
|
||||
updateContext.alreadyErased++;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!localEC || localEC.utcDateChanged <= remoteEC.utcDateChanged) {
|
||||
if (!remoteEntityRow) {
|
||||
throw new Error(`Empty entity row for: ${JSON.stringify(remoteEC)}`);
|
||||
}
|
||||
|
||||
if (remoteEC.entityName === 'blobs' && remoteEntityRow.content !== null) {
|
||||
// we always use a Buffer object which is different from normal saving - there we use a simple string type for
|
||||
// "string notes". The problem is that in general, it's not possible to detect whether a blob content
|
||||
@@ -61,6 +103,9 @@ function updateNormalEntity(remoteEC, remoteEntityRow, instanceId) {
|
||||
|
||||
sql.replace(remoteEC.entityName, remoteEntityRow);
|
||||
|
||||
updateContext.updated[remoteEC.entityName] = updateContext.updated[remoteEC.entityName] || [];
|
||||
updateContext.updated[remoteEC.entityName].push(remoteEC.entityId);
|
||||
|
||||
if (!localEC || localEC.utcDateChanged < remoteEC.utcDateChanged) {
|
||||
entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId);
|
||||
}
|
||||
@@ -77,6 +122,10 @@ function updateNormalEntity(remoteEC, remoteEntityRow, instanceId) {
|
||||
}
|
||||
|
||||
function updateNoteReordering(remoteEC, remoteEntityRow, instanceId) {
|
||||
if (!remoteEntityRow) {
|
||||
throw new Error(`Empty note_reordering body for: ${JSON.stringify(remoteEC)}`);
|
||||
}
|
||||
|
||||
for (const key in remoteEntityRow) {
|
||||
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [remoteEntityRow[key], key]);
|
||||
}
|
||||
@@ -110,6 +159,15 @@ function eraseEntity(entityChange, instanceId) {
|
||||
entityChangesService.putEntityChangeWithInstanceId(entityChange, instanceId);
|
||||
}
|
||||
|
||||
function logUpdateContext(updateContext) {
|
||||
const message = JSON.stringify(updateContext)
|
||||
.replaceAll('"', '')
|
||||
.replaceAll(":", ": ")
|
||||
.replaceAll(",", ", ");
|
||||
|
||||
log.info(message.substr(1, message.length - 2));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
updateEntity
|
||||
updateEntities
|
||||
};
|
||||
|
||||
@@ -25,6 +25,10 @@ function md5(content) {
|
||||
}
|
||||
|
||||
function hashedBlobId(content) {
|
||||
if (content === null || content === undefined) {
|
||||
content = "";
|
||||
}
|
||||
|
||||
// sha512 is faster than sha256
|
||||
const base64Hash = crypto.createHash('sha512').update(content).digest('base64');
|
||||
|
||||
@@ -289,6 +293,16 @@ function normalize(str) {
|
||||
return removeDiacritic(str).toLowerCase();
|
||||
}
|
||||
|
||||
function toMap(list, key) {
|
||||
const map = {};
|
||||
|
||||
for (const el of list) {
|
||||
map[el[key]] = el;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
randomSecureToken,
|
||||
randomString,
|
||||
@@ -320,4 +334,5 @@ module.exports = {
|
||||
removeDiacritic,
|
||||
normalize,
|
||||
hashedBlobId,
|
||||
toMap,
|
||||
};
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
/**
|
||||
* this is used as a "standalone js" file and required by a shared note directly via script-tags
|
||||
*
|
||||
* data input comes via window variable as follows
|
||||
* const {elements, appState, files} = window.triliumExcalidraw;
|
||||
*/
|
||||
|
||||
document.getElementById("excalidraw-app").style.height = `${appState.height}px`;
|
||||
|
||||
const App = () => {
|
||||
const excalidrawRef = React.useRef(null);
|
||||
const excalidrawWrapperRef = React.useRef(null);
|
||||
const [dimensions, setDimensions] = React.useState({
|
||||
width: undefined,
|
||||
height: appState.height,
|
||||
});
|
||||
const [viewModeEnabled, setViewModeEnabled] = React.useState(false);
|
||||
|
||||
// ensure that assets are loaded from trilium
|
||||
|
||||
/**
|
||||
* resizing
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
const dimensions = {
|
||||
width: excalidrawWrapperRef.current.getBoundingClientRect().width,
|
||||
height: excalidrawWrapperRef.current.getBoundingClientRect().height
|
||||
};
|
||||
setDimensions(dimensions);
|
||||
|
||||
const onResize = () => {
|
||||
const dimensions = {
|
||||
width: excalidrawWrapperRef.current.getBoundingClientRect().width,
|
||||
height: excalidrawWrapperRef.current.getBoundingClientRect().height
|
||||
};
|
||||
setDimensions(dimensions);
|
||||
};
|
||||
|
||||
window.addEventListener("resize", onResize);
|
||||
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}, [excalidrawWrapperRef]);
|
||||
|
||||
return React.createElement(
|
||||
React.Fragment,
|
||||
null,
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "excalidraw-wrapper",
|
||||
ref: excalidrawWrapperRef
|
||||
},
|
||||
React.createElement(ExcalidrawLib.Excalidraw, {
|
||||
ref: excalidrawRef,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
initialData: {
|
||||
elements, appState, files
|
||||
},
|
||||
viewModeEnabled: !viewModeEnabled,
|
||||
zenModeEnabled: false,
|
||||
gridModeEnabled: false,
|
||||
isCollaborating: false,
|
||||
detectScroll: false,
|
||||
handleKeyboardGlobally: false,
|
||||
autoFocus: true,
|
||||
renderFooter: () => {
|
||||
return React.createElement(
|
||||
React.Fragment,
|
||||
null,
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "excalidraw-top-right-ui excalidraw Island",
|
||||
},
|
||||
React.createElement(
|
||||
"label",
|
||||
{
|
||||
style: {
|
||||
padding: "5px",
|
||||
},
|
||||
className: "excalidraw Stack",
|
||||
},
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: () => setViewModeEnabled(!viewModeEnabled)
|
||||
},
|
||||
viewModeEnabled ? " Enter simple view mode " : " Enter extended view mode "
|
||||
),
|
||||
""
|
||||
),
|
||||
));
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
ReactDOM.render(React.createElement(App), document.getElementById("excalidraw-app"));
|
||||
@@ -25,14 +25,12 @@ function getContent(note) {
|
||||
renderCode(result);
|
||||
} else if (note.type === 'mermaid') {
|
||||
renderMermaid(result);
|
||||
} else if (note.type === 'image') {
|
||||
} else if (note.type === 'image' || note.type === 'canvas') {
|
||||
renderImage(result, note);
|
||||
} else if (note.type === 'file') {
|
||||
renderFile(note, result);
|
||||
} else if (note.type === 'book') {
|
||||
result.isEmpty = true;
|
||||
} else if (note.type === 'canvas') {
|
||||
renderCanvas(result, note);
|
||||
} else {
|
||||
result.content = '<p>This note type cannot be displayed.</p>';
|
||||
}
|
||||
@@ -151,39 +149,6 @@ function renderFile(note, result) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderCanvas(result, note) {
|
||||
result.header += `<script>
|
||||
window.EXCALIDRAW_ASSET_PATH = window.location.origin + "/node_modules/@excalidraw/excalidraw/dist/";
|
||||
</script>`;
|
||||
result.header += `<script src="../../${assetPath}/node_modules/react/umd/react.production.min.js"></script>`;
|
||||
result.header += `<script src="../../${assetPath}/node_modules/react-dom/umd/react-dom.production.min.js"></script>`;
|
||||
result.header += `<script src="../../${assetPath}/node_modules/@excalidraw/excalidraw/dist/excalidraw.production.min.js"></script>`;
|
||||
result.header += `<style>
|
||||
|
||||
.excalidraw-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:root[dir="ltr"]
|
||||
.excalidraw
|
||||
.layer-ui__wrapper
|
||||
.zen-mode-transition.App-menu_bottom--transition-left {
|
||||
transform: none;
|
||||
}
|
||||
</style>`;
|
||||
|
||||
result.content = `<div>
|
||||
<script>
|
||||
const {elements, appState, files} = JSON.parse(${JSON.stringify(result.content)});
|
||||
window.triliumExcalidraw = {elements, appState, files}
|
||||
</script>
|
||||
<div id="excalidraw-app"></div>
|
||||
<hr>
|
||||
<a href="api/images/${note.noteId}/${note.escapedTitle}?utc=${note.utcDateModified}">Get Image Link</a>
|
||||
<script src="./canvas_share.js"></script>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getContent
|
||||
};
|
||||
|
||||
@@ -142,8 +142,6 @@ function register(router) {
|
||||
});
|
||||
}
|
||||
|
||||
router.use('/share/canvas_share.js', express.static(path.join(__dirname, 'canvas_share.js')));
|
||||
|
||||
router.get('/share/', (req, res, next) => {
|
||||
if (req.path.substr(-1) !== '/') {
|
||||
res.redirect('../share/');
|
||||
@@ -219,19 +217,24 @@ function register(router) {
|
||||
* special "image" type. the canvas is actually type application/json
|
||||
* to avoid bitrot and enable usage as referenced image the svg is included.
|
||||
*/
|
||||
const content = image.getContent();
|
||||
try {
|
||||
const data = JSON.parse(content);
|
||||
let svgString = '<svg/>'
|
||||
const attachment = image.getAttachmentByTitle('canvas-export.svg');
|
||||
|
||||
const svg = data.svg || '<svg />';
|
||||
addNoIndexHeader(image, res);
|
||||
res.set('Content-Type', "image/svg+xml");
|
||||
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.send(svg);
|
||||
} catch (err) {
|
||||
res.status(500)
|
||||
.json({ message: "There was an error parsing excalidraw to svg." });
|
||||
if (attachment) {
|
||||
svgString = attachment.getContent();
|
||||
} else {
|
||||
// backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key
|
||||
const contentSvg = image.getJsonContentSafely()?.svg;
|
||||
|
||||
if (contentSvg) {
|
||||
svgString = contentSvg;
|
||||
}
|
||||
}
|
||||
|
||||
const svg = svgString
|
||||
res.set('Content-Type', "image/svg+xml");
|
||||
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.send(svg);
|
||||
} else {
|
||||
// normal image
|
||||
res.set('Content-Type', image.mime);
|
||||
|
||||
@@ -470,6 +470,11 @@ class SNote extends AbstractShacaEntity {
|
||||
return this.attachments;
|
||||
}
|
||||
|
||||
/** @returns {SAttachment} */
|
||||
getAttachmentByTitle(title) {
|
||||
return this.attachments.find(attachment => attachment.title === title);
|
||||
}
|
||||
|
||||
/** @returns {string} */
|
||||
get shareId() {
|
||||
if (this.hasOwnedLabel('shareRoot')) {
|
||||
|
||||
Reference in New Issue
Block a user