Compare commits

...

20 Commits

Author SHA1 Message Date
zadam
48248224f8 release 0.61.7-beta 2023-09-21 23:38:18 +02:00
zadam
79e5e3b65f more logging for sync 2023-09-21 18:13:14 +02:00
zadam
d0383f3044 remove debian packaging hack 2023-09-21 12:44:20 +02:00
zadam
62ecd1ad76 node 18.18.0 2023-09-21 12:29:11 +02:00
zadam
75b7706c2c mermaid 10.4.0 2023-09-21 12:24:44 +02:00
zadam
584b176643 hiding useless/misleading buttons from excalidraw 2023-09-21 12:03:40 +02:00
zadam
a16555bbff don't save appState in canvas - app state should be reset after each load 2023-09-21 11:54:54 +02:00
zadam
a749e24147 fix creating revisions for non-text notes #4176 2023-09-21 11:38:38 +02:00
zadam
bb81f110dd improved error handling 2023-09-21 11:16:03 +02:00
zadam
5dd6f49104 chart js 0.16.0 2023-09-19 23:59:05 +02:00
zadam
bc6be44b19 better error condition checks 2023-09-19 23:48:55 +02:00
zadam
8075265753 fix migration, closes #4262 2023-09-18 23:45:00 +02:00
zadam
4a67f63abd better error message in image attachment + upgrades 2023-09-14 00:18:56 +02:00
zadam
d9666210f1 make sure that the hidden note is never selected when hoisted to root to avoid e.g. erroneous deletion 2023-09-13 16:57:24 +02:00
zadam
5f6d562bf8 add ability to excalidraw to follow links 2023-09-08 23:00:43 +02:00
zadam
f37dc66074 add support for storing canvas libraries in note attachments plus storing exported SVG in attachment 2023-09-08 21:53:57 +02:00
zadam
0b84524807 fix refreshing note actions on note/tab switch, closes #4247 2023-09-08 00:43:18 +02:00
zadam
8062bb7e2a fix reloading unprotected note title to the document.title after entering protected session, fixes #4244 2023-09-08 00:28:00 +02:00
zadam
806062c8d1 fix undelete attachments 2023-09-08 00:22:48 +02:00
zadam
83f19c0537 fix protecting attachments 2023-09-08 00:19:30 +02:00
41 changed files with 639 additions and 554 deletions

View File

@@ -75,7 +75,6 @@ module.exports = {
glob: true,
log: true,
EditorWatchdog: true,
// \src\share\canvas_share.js
React: true,
appState: true,
ExcalidrawLib: true,

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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" ]
}
}

View File

@@ -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.

File diff suppressed because one or more lines are too long

120
package-lock.json generated
View File

@@ -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": {

View File

@@ -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",

View File

@@ -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;

View File

@@ -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;
});
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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';

View File

@@ -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) {

View File

@@ -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">')

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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(() => {

View File

@@ -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" };

View File

@@ -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`);
}
});

View File

@@ -183,5 +183,6 @@ module.exports = {
eraseDeletedNotesNow,
eraseUnusedAttachmentsNow,
eraseNotesWithDeleteId,
eraseUnusedBlobs
eraseUnusedBlobs,
eraseAttachments
};

View File

@@ -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]);

View File

@@ -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);

View File

@@ -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',

View File

@@ -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;

View File

@@ -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);

View File

@@ -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
};

View File

@@ -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,
};

View File

@@ -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"));

View File

@@ -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
};

View File

@@ -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);

View File

@@ -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')) {