Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ddc79b2517 | |||
| 8b250ed523 | |||
| d8b78d8025 | |||
| 42112b8053 | |||
| 6cc0dd5a80 | |||
| afd5f4823f | |||
| b0cf82c91b | |||
| 6a67cdd5af | |||
| bad7b84993 | |||
| d3ca6b5ae6 | |||
| da5009f089 | |||
| c08524c977 | |||
| f89537037e | |||
| c153793766 | |||
| 0aec5927d5 | |||
| 8aea9a1801 | |||
| 73247e3220 | |||
| 89344a6eda | |||
| 40d2e6ea83 | |||
| 910cfe9a17 | |||
| e58a80fc00 | |||
| 4a2319cb33 | |||
| 5619088c41 | |||
| 60271993eb | |||
| 6695e8b011 | |||
| 707df18b93 | |||
| 90895f1288 | |||
| ba1ca506af | |||
| f90ed99a40 | |||
| 67630b1a22 | |||
| 2c1580ea65 | |||
| 840a0b5f64 | |||
| b39f6ef7ad | |||
| fb27088fcd | |||
| 76fbff68ba | |||
| 54de4d236d | |||
| e211dd65ad | |||
| a87f4d8653 | |||
| b59c175c2e | |||
| 580104c4c5 | |||
| 0fc3053b0a | |||
| 929e0f69c2 | |||
| e70af1300a | |||
| 4d0e46021b | |||
| 1a9a49b739 | |||
| 2ac560c56e | |||
| c23387c0fb | |||
| 0ff250fe15 | |||
| 52b1e58b26 | |||
| 484715e440 | |||
| 9d42c3d802 | |||
| c654172d33 | |||
| e17b26c883 | |||
| 24d02d9cf5 | |||
| 7208a311ac | |||
| ad7355372b | |||
| f18b5babad | |||
| 9831ec0ca9 | |||
| 596544eca3 | |||
| 1f853024ee | |||
| 0308b13460 | |||
| 06b8a82f70 | |||
| 91ca07929d | |||
| afabaa5fdb | |||
| a6fd3fa77c | |||
| 58a2c08dcd | |||
| 299bbff2f4 | |||
| 19d8947123 | |||
| 35edce7523 | |||
| bc4cec69a5 | |||
| cce8c1b674 | |||
| aa58788769 | |||
| 6c62ab7a52 | |||
| d6ab638b30 | |||
| bd4db406de | |||
| e50f9cd0a3 | |||
| 2b64cbce2c | |||
| 2797c942ab | |||
| f1c3278874 | |||
| 424c22dcde | |||
| 5c223dfd12 | |||
| 6a3e7a5a8e | |||
| f88cdac000 | |||
| eeead90f32 | |||
| b607857409 | |||
| 9268f88bc3 | |||
| f7f0560a9f | |||
| 3d8905207e | |||
| dbc312010b | |||
| b115a7cf19 | |||
| 348562352c | |||
| 70fd917e7c | |||
| d2b60764cd | |||
| 581b1fdaa5 | |||
| 3c19a712c0 | |||
| cc27f16088 | |||
| dffdb82288 | |||
| 62b44e3549 |
@@ -3,22 +3,22 @@
|
||||
BUILD_DIR=./dist/trilium-linux-x64
|
||||
rm -rf $BUILD_DIR
|
||||
|
||||
# we build x64 as second so that we keep X64 binaries in node_modules for local development and server build
|
||||
echo "Rebuilding binaries for linux-x64"
|
||||
./node_modules/.bin/electron-rebuild --arch=x64
|
||||
rm -r node_modules/sqlite3/lib/binding/*
|
||||
|
||||
./node_modules/.bin/electron-packager . --out=dist --executable-name=trilium --platform=linux --arch=x64 --overwrite
|
||||
cp -r bin/deps/linux-x64/sqlite/* node_modules/sqlite3/lib/binding/
|
||||
|
||||
./node_modules/.bin/electron-packager . --asar --out=dist --executable-name=trilium --platform=linux --arch=x64 --overwrite
|
||||
|
||||
mv "./dist/Trilium Notes-linux-x64" $BUILD_DIR
|
||||
|
||||
rm -r "$BUILD_DIR/resources/app/node_modules/sqlite3/lib/binding/*"
|
||||
cp src/public/images/app-icons/png/128x128.png $BUILD_DIR/icon.png
|
||||
|
||||
cp -r bin/deps/linux-x64/sqlite/electron* "$BUILD_DIR/resources/app/node_modules/sqlite3/lib/binding/"
|
||||
|
||||
rm -r $BUILD_DIR/resources/app/bin/deps
|
||||
# removing software WebGL binaries because they are pretty huge and not necessary
|
||||
rm -r $BUILD_DIR/swiftshader
|
||||
|
||||
echo "Packaging linux x64 electron distribution..."
|
||||
VERSION=`jq -r ".version" package.json`
|
||||
7z a $BUILD_DIR-${VERSION}.7z $BUILD_DIR
|
||||
|
||||
cd dist
|
||||
|
||||
tar cJf trilium-linux-x64-${VERSION}.tar.xz trilium-linux-x64
|
||||
|
||||
+20
-15
@@ -3,25 +3,30 @@
|
||||
BUILD_DIR=./dist/trilium-mac-x64
|
||||
rm -rf $BUILD_DIR
|
||||
|
||||
./node_modules/.bin/electron-packager . --out=dist --executable-name=trilium --platform=darwin --arch=x64 --overwrite --icon=src/public/images/app-icons/mac/icon.icns
|
||||
echo "Copying required mac binaries"
|
||||
|
||||
rm -r node_modules/sqlite3/lib/binding/*
|
||||
rm -r node_modules/mozjpeg/vendor/*
|
||||
rm -r node_modules/pngquant-bin/vendor/*
|
||||
rm -r node_modules/giflossy/vendor/*
|
||||
|
||||
cp -r bin/deps/mac-x64/sqlite/* node_modules/sqlite3/lib/binding/
|
||||
cp bin/deps/mac-x64/image/cjpeg node_modules/mozjpeg/vendor/
|
||||
cp bin/deps/mac-x64/image/pngquant node_modules/pngquant-bin/vendor/
|
||||
cp bin/deps/mac-x64/image/gifsicle node_modules/giflossy/vendor/
|
||||
|
||||
./node_modules/.bin/electron-packager . --asar --out=dist --executable-name=trilium --platform=darwin --arch=x64 --overwrite --icon=src/public/images/app-icons/mac/icon.icns
|
||||
|
||||
# Mac build has by default useless directory level
|
||||
mv "./dist/Trilium Notes-darwin-x64" $BUILD_DIR
|
||||
|
||||
echo "Copying required mac binaries"
|
||||
./reset-local.sh
|
||||
|
||||
MAC_RES_DIR=$BUILD_DIR/Trilium\ Notes.app/Contents/Resources/app
|
||||
|
||||
rm -r "$MAC_RES_DIR/node_modules/sqlite3/lib/binding/*"
|
||||
|
||||
cp -r bin/deps/mac-x64/sqlite/* "$MAC_RES_DIR/node_modules/sqlite3/lib/binding/"
|
||||
cp bin/deps/mac-x64/image/cjpeg "$MAC_RES_DIR/node_modules/mozjpeg/vendor/"
|
||||
cp bin/deps/mac-x64/image/pngquant "$MAC_RES_DIR/node_modules/pngquant-bin/vendor/"
|
||||
cp bin/deps/mac-x64/image/gifsicle "$MAC_RES_DIR/node_modules/giflossy/vendor/"
|
||||
|
||||
rm -r "$MAC_RES_DIR/bin/deps"
|
||||
|
||||
echo "Packaging mac x64 electron distribution..."
|
||||
echo "Zipping mac x64 electron distribution..."
|
||||
|
||||
VERSION=`jq -r ".version" package.json`
|
||||
7z a $BUILD_DIR-${VERSION}.7z $BUILD_DIR
|
||||
|
||||
cd dist
|
||||
|
||||
rm trilium-mac-x64-${VERSION}.zip
|
||||
zip -r9 --symlinks trilium-mac-x64-${VERSION}.zip trilium-mac-x64
|
||||
|
||||
+2
-1
@@ -34,4 +34,5 @@ chmod 755 trilium.sh
|
||||
cd ..
|
||||
|
||||
VERSION=`jq -r ".version" ../package.json`
|
||||
7z a trilium-linux-x64-server-${VERSION}.7z trilium-linux-x64-server
|
||||
|
||||
tar cJf trilium-linux-x64-server-${VERSION}.tar.xz trilium-linux-x64-server
|
||||
+20
-13
@@ -3,23 +3,30 @@
|
||||
BUILD_DIR=./dist/trilium-windows-x64
|
||||
rm -rf $BUILD_DIR
|
||||
|
||||
./node_modules/.bin/electron-packager . --out=dist --executable-name=trilium --platform=win32 --arch=x64 --overwrite --icon=src/public/images/app-icons/win/icon.ico
|
||||
echo "Copying required windows binaries"
|
||||
|
||||
rm -r node_modules/sqlite3/lib/binding/*
|
||||
rm -r node_modules/mozjpeg/vendor/*
|
||||
rm -r node_modules/pngquant-bin/vendor/*
|
||||
rm -r node_modules/giflossy/vendor/*
|
||||
|
||||
cp -r bin/deps/win-x64/sqlite/* node_modules/sqlite3/lib/binding/
|
||||
cp bin/deps/win-x64/image/cjpeg.exe node_modules/mozjpeg/vendor/
|
||||
cp bin/deps/win-x64/image/pngquant.exe node_modules/pngquant-bin/vendor/
|
||||
cp bin/deps/win-x64/image/gifsicle.exe node_modules/giflossy/vendor/
|
||||
|
||||
./node_modules/.bin/electron-packager . --asar --out=dist --executable-name=trilium --platform=win32 --arch=x64 --overwrite --icon=src/public/images/app-icons/win/icon.ico
|
||||
|
||||
mv "./dist/Trilium Notes-win32-x64" $BUILD_DIR
|
||||
|
||||
echo "Copying required windows binaries"
|
||||
|
||||
WIN_RES_DIR=$BUILD_DIR/resources/app
|
||||
|
||||
cp -r bin/deps/win-x64/sqlite/* $WIN_RES_DIR/node_modules/sqlite3/lib/binding/
|
||||
cp bin/deps/win-x64/image/cjpeg.exe $WIN_RES_DIR/node_modules/mozjpeg/vendor/
|
||||
cp bin/deps/win-x64/image/pngquant.exe $WIN_RES_DIR/node_modules/pngquant-bin/vendor/
|
||||
cp bin/deps/win-x64/image/gifsicle.exe $WIN_RES_DIR/node_modules/giflossy/vendor/
|
||||
|
||||
rm -r $WIN_RES_DIR/bin/deps
|
||||
# removing software WebGL binaries because they are pretty huge and not necessary
|
||||
rm -r $BUILD_DIR/swiftshader
|
||||
|
||||
echo "Packaging windows x64 electron distribution..."
|
||||
./reset-local.sh
|
||||
|
||||
echo "Zipping windows x64 electron distribution..."
|
||||
VERSION=`jq -r ".version" package.json`
|
||||
7z a $BUILD_DIR-${VERSION}.7z $BUILD_DIR
|
||||
|
||||
cd dist
|
||||
|
||||
zip -r9 trilium-windows-x64-${VERSION}.zip trilium-windows-x64
|
||||
|
||||
+4
-4
@@ -42,10 +42,10 @@ git push origin $TAG
|
||||
|
||||
bin/build.sh
|
||||
|
||||
LINUX_X64_BUILD=trilium-linux-x64-$VERSION.7z
|
||||
WINDOWS_X64_BUILD=trilium-windows-x64-$VERSION.7z
|
||||
MAC_X64_BUILD=trilium-mac-x64-$VERSION.7z
|
||||
SERVER_BUILD=trilium-linux-x64-server-$VERSION.7z
|
||||
LINUX_X64_BUILD=trilium-linux-x64-$VERSION.tar.xz
|
||||
WINDOWS_X64_BUILD=trilium-windows-x64-$VERSION.zip
|
||||
MAC_X64_BUILD=trilium-mac-x64-$VERSION.zip
|
||||
SERVER_BUILD=trilium-linux-x64-server-$VERSION.tar.xz
|
||||
|
||||
echo "Creating release in GitHub"
|
||||
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
./node_modules/.bin/electron-rebuild --arch=x64
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,62 @@
|
||||
const sql = require('../../src/services/sql');
|
||||
|
||||
function prependIv(cipherText, ivText) {
|
||||
const arr = ivText.split("").map(c => parseInt(c) || 0);
|
||||
const iv = Buffer.from(arr);
|
||||
const payload = Buffer.from(cipherText, 'base64');
|
||||
const complete = Buffer.concat([iv, payload]);
|
||||
|
||||
return complete.toString('base64');
|
||||
}
|
||||
|
||||
async function updateEncryptedDataKey() {
|
||||
const encryptedDataKey = await sql.getValue("SELECT value FROM options WHERE name = 'encryptedDataKey'");
|
||||
const encryptedDataKeyIv = await sql.getValue("SELECT value FROM options WHERE name = 'encryptedDataKeyIv'");
|
||||
|
||||
const newEncryptedDataKey = prependIv(encryptedDataKey, encryptedDataKeyIv);
|
||||
|
||||
await sql.execute("UPDATE options SET value = ? WHERE name = 'encryptedDataKey'", [newEncryptedDataKey]);
|
||||
|
||||
await sql.execute("DELETE FROM options WHERE name = 'encryptedDataKeyIv'");
|
||||
await sql.execute("DELETE FROM sync WHERE entityName = 'options' AND entityId = 'encryptedDataKeyIv'");
|
||||
}
|
||||
|
||||
async function updateNotes() {
|
||||
const protectedNotes = await sql.getRows("SELECT noteId, title, content FROM notes WHERE isProtected = 1");
|
||||
|
||||
for (const note of protectedNotes) {
|
||||
if (note.title !== null) {
|
||||
note.title = prependIv(note.title, "0" + note.noteId);
|
||||
}
|
||||
|
||||
if (note.content !== null) {
|
||||
note.content = prependIv(note.content, "1" + note.noteId);
|
||||
}
|
||||
|
||||
await sql.execute("UPDATE notes SET title = ?, content = ? WHERE noteId = ?", [note.title, note.content, note.noteId]);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateNoteRevisions() {
|
||||
const protectedNoteRevisions = await sql.getRows("SELECT noteRevisionId, title, content FROM note_revisions WHERE isProtected = 1");
|
||||
|
||||
for (const noteRevision of protectedNoteRevisions) {
|
||||
if (noteRevision.title !== null) {
|
||||
noteRevision.title = prependIv(noteRevision.title, "0" + noteRevision.noteRevisionId);
|
||||
}
|
||||
|
||||
if (noteRevision.content !== null) {
|
||||
noteRevision.content = prependIv(noteRevision.content, "1" + noteRevision.noteRevisionId);
|
||||
}
|
||||
|
||||
await sql.execute("UPDATE note_revisions SET title = ?, content = ? WHERE noteRevisionId = ?", [noteRevision.title, noteRevision.content, noteRevision.noteRevisionId]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = async () => {
|
||||
await updateEncryptedDataKey();
|
||||
|
||||
await updateNotes();
|
||||
|
||||
await updateNoteRevisions();
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
VALUES ('mainFontSize', '100', '2019-01-13T18:31:00.874Z', '2019-01-13T18:31:00.874Z', 0);
|
||||
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
VALUES ('treeFontSize', '100', '2019-01-13T18:31:00.874Z', '2019-01-13T18:31:00.874Z', 0);
|
||||
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
VALUES ('detailFontSize', '110', '2019-01-13T18:31:00.874Z', '2019-01-13T18:31:00.874Z', 0);
|
||||
@@ -0,0 +1,11 @@
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
SELECT 'mainFontSize', '100', '2019-01-13T18:31:00.874Z', '2019-01-13T18:31:00.874Z', 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM options WHERE name = 'mainFontSize');
|
||||
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
SELECT 'treeFontSize', '100', '2019-01-13T18:31:00.874Z', '2019-01-13T18:31:00.874Z', 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM options WHERE name = 'treeFontSize');
|
||||
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
SELECT 'detailFontSize', '110', '2019-01-13T18:31:00.874Z', '2019-01-13T18:31:00.874Z', 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM options WHERE name = 'detailFontSize');
|
||||
+1
-1
@@ -81,7 +81,7 @@ app.on('ready', async () => {
|
||||
const dateNoteService = require('./src/services/date_notes');
|
||||
const dateUtils = require('./src/services/date_utils');
|
||||
|
||||
const parentNote = await dateNoteService.getDateNote(dateUtils.nowDate());
|
||||
const parentNote = await dateNoteService.getDateNote(dateUtils.nowLocalDate());
|
||||
|
||||
// window may be hidden / not in focus
|
||||
mainWindow.focus();
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
For bug reports, please mention **version of the application** and include **log files** from following location:
|
||||
|
||||
* `/home/[user]/.local/share/trilium-data/log` for Linux
|
||||
* `C:\Users\[user]\AppData\Roaming\trilium-data\log` for Windows Vista and up
|
||||
* `/Users/[user]/Library/Application Support/trilium-data/log` for Mac OS
|
||||
Generated
+1843
-487
File diff suppressed because it is too large
Load Diff
+20
-17
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.27.4",
|
||||
"version": "0.29.1",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
@@ -17,7 +17,8 @@
|
||||
"start-electron": "electron . --disable-gpu",
|
||||
"build-backend-docs": "jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js",
|
||||
"build-frontend-docs": "jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/javascripts/entities/*.js src/public/javascripts/services/frontend_script_api.js",
|
||||
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs"
|
||||
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs",
|
||||
"postinstall": "electron-builder install-app-deps"
|
||||
},
|
||||
"dependencies": {
|
||||
"async-mutex": "0.1.3",
|
||||
@@ -28,52 +29,54 @@
|
||||
"cookie-parser": "1.4.3",
|
||||
"debug": "4.1.1",
|
||||
"ejs": "2.6.1",
|
||||
"electron-debug": "2.0.0",
|
||||
"electron-dl": "1.12.0",
|
||||
"electron-debug": "2.1.0",
|
||||
"electron-dl": "1.13.0",
|
||||
"electron-in-page-search": "1.3.2",
|
||||
"express": "4.16.4",
|
||||
"express-session": "1.15.6",
|
||||
"file-type": "10.7.0",
|
||||
"file-type": "10.7.1",
|
||||
"fs-extra": "7.0.1",
|
||||
"get-port": "4.1.0",
|
||||
"helmet": "3.15.0",
|
||||
"html": "1.0.0",
|
||||
"image-type": "3.0.0",
|
||||
"imagemin": "6.0.0",
|
||||
"imagemin": "6.1.0",
|
||||
"imagemin-giflossy": "5.1.10",
|
||||
"imagemin-mozjpeg": "8.0.0",
|
||||
"imagemin-pngquant": "6.0.0",
|
||||
"imagemin-pngquant": "7.0.0",
|
||||
"ini": "1.3.5",
|
||||
"jimp": "0.6.0",
|
||||
"mime-types": "^2.1.21",
|
||||
"moment": "2.23.0",
|
||||
"moment": "2.24.0",
|
||||
"multer": "1.4.1",
|
||||
"node-abi": "2.5.1",
|
||||
"node-abi": "2.6.0",
|
||||
"open": "0.0.5",
|
||||
"rand-token": "0.4.0",
|
||||
"rcedit": "1.1.1",
|
||||
"rimraf": "2.6.2",
|
||||
"rimraf": "2.6.3",
|
||||
"sanitize-filename": "1.6.1",
|
||||
"sax": "^1.2.4",
|
||||
"semver": "^5.6.0",
|
||||
"serve-favicon": "2.5.0",
|
||||
"session-file-store": "1.2.0",
|
||||
"simple-node-logger": "18.12.21",
|
||||
"sqlite": "3.0.0",
|
||||
"sqlite": "3.0.1",
|
||||
"tar-stream": "1.6.2",
|
||||
"turndown": "5.0.1",
|
||||
"turndown": "5.0.3",
|
||||
"unescape": "1.0.1",
|
||||
"ws": "6.1.2",
|
||||
"ws": "6.1.3",
|
||||
"xml2js": "0.4.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"devtron": "1.4.0",
|
||||
"electron": "4.0.1",
|
||||
"electron-compile": "6.4.3",
|
||||
"electron": "4.0.3",
|
||||
"electron-builder": "20.38.5",
|
||||
"electron-compile": "6.4.4",
|
||||
"electron-packager": "13.0.1",
|
||||
"electron-rebuild": "1.8.2",
|
||||
"lorem-ipsum": "1.0.6",
|
||||
"tape": "4.9.1",
|
||||
"xo": "0.23.0"
|
||||
"tape": "4.9.2",
|
||||
"xo": "0.24.0"
|
||||
},
|
||||
"xo": {
|
||||
"envs": [
|
||||
|
||||
+3
-1
@@ -39,7 +39,7 @@ app.use((req, res, next) => {
|
||||
});
|
||||
});
|
||||
|
||||
app.use(bodyParser.json({limit: '50mb'}));
|
||||
app.use(bodyParser.json({limit: '500mb'}));
|
||||
app.use(bodyParser.urlencoded({extended: false}));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
@@ -63,6 +63,8 @@ app.use(favicon(__dirname + '/public/images/app-icons/win/icon.ico'));
|
||||
|
||||
require('./routes/routes').register(app);
|
||||
|
||||
require('./routes/custom').register(app);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use((req, res, next) => {
|
||||
const err = new Error('Router not found for request ' + req.url);
|
||||
|
||||
+22
-2
@@ -47,7 +47,18 @@ class Note extends Entity {
|
||||
if (this.isProtected && this.noteId) {
|
||||
this.isContentAvailable = protectedSessionService.isProtectedSessionAvailable();
|
||||
|
||||
protectedSessionService.decryptNote(this);
|
||||
if (this.isContentAvailable) {
|
||||
protectedSessionService.decryptNote(this);
|
||||
}
|
||||
else {
|
||||
// saving ciphertexts in case we do want to update protected note outside of protected session
|
||||
// (which is allowed)
|
||||
this.titleCipherText = this.title;
|
||||
this.contentCipherText = this.content;
|
||||
|
||||
this.title = "[protected]";
|
||||
this.content = "";
|
||||
}
|
||||
}
|
||||
|
||||
this.setContent(this.content);
|
||||
@@ -629,12 +640,21 @@ class Note extends Entity {
|
||||
// cannot be static!
|
||||
updatePojo(pojo) {
|
||||
if (pojo.isProtected) {
|
||||
protectedSessionService.encryptNote(pojo);
|
||||
if (this.isContentAvailable) {
|
||||
protectedSessionService.encryptNote(pojo);
|
||||
}
|
||||
else {
|
||||
// updating protected note outside of protected session means we will keep original ciphertexts
|
||||
pojo.title = pojo.titleCipherText;
|
||||
pojo.content = pojo.contentCipherText;
|
||||
}
|
||||
}
|
||||
|
||||
delete pojo.jsonContent;
|
||||
delete pojo.isContentAvailable;
|
||||
delete pojo.__attributeCache;
|
||||
delete pojo.titleCipherText;
|
||||
delete pojo.contentCipherText;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
@@ -37,6 +37,7 @@ import noteTypeService from './services/note_type.js';
|
||||
import linkService from './services/link.js';
|
||||
import noteAutocompleteService from './services/note_autocomplete.js';
|
||||
import macInit from './services/mac_init.js';
|
||||
import cssLoader from './services/css_loader.js';
|
||||
|
||||
// required for CKEditor image upload plugin
|
||||
window.glob.getCurrentNode = treeService.getCurrentNode;
|
||||
@@ -79,6 +80,10 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||
return false;
|
||||
};
|
||||
|
||||
for (const appCssNoteId of window.appCssNoteIds) {
|
||||
cssLoader.requireCss(`/api/notes/download/${appCssNoteId}`);
|
||||
}
|
||||
|
||||
const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/";
|
||||
|
||||
$(document).on("click", "button[data-help-page]", e => {
|
||||
@@ -121,6 +126,8 @@ $("#export-note-button").click(function () {
|
||||
|
||||
macInit.init();
|
||||
|
||||
searchNotesService.init(); // should be in front of treeService since that one manipulates address bar hash
|
||||
|
||||
treeService.showTree();
|
||||
|
||||
entrypoints.registerEntrypoints();
|
||||
|
||||
@@ -97,7 +97,7 @@ function AttributesModel() {
|
||||
await showAttributes(attributes);
|
||||
|
||||
// attribute might not be rendered immediatelly so could not focus
|
||||
setTimeout(() => $(".attribute-type-select:last").focus(), 100);
|
||||
setTimeout(() => $(".attribute-type-select:last").focus(), 1000);
|
||||
};
|
||||
|
||||
this.deleteAttribute = function(data, event) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import server from '../services/server.js';
|
||||
import infoService from "../services/info.js";
|
||||
import zoomService from "../services/zoom.js";
|
||||
import utils from "../services/utils.js";
|
||||
import cssLoader from "../services/css_loader.js";
|
||||
|
||||
const $dialog = $("#options-dialog");
|
||||
|
||||
@@ -44,10 +45,28 @@ addTabHandler((function() {
|
||||
const $zoomFactorSelect = $("#zoom-factor-select");
|
||||
const $leftPaneMinWidth = $("#left-pane-min-width");
|
||||
const $leftPaneWidthPercent = $("#left-pane-width-percent");
|
||||
const $html = $("html");
|
||||
const $mainFontSize = $("#main-font-size");
|
||||
const $treeFontSize = $("#tree-font-size");
|
||||
const $detailFontSize = $("#detail-font-size");
|
||||
const $body = $("body");
|
||||
const $container = $("#container");
|
||||
|
||||
function optionsLoaded(options) {
|
||||
async function optionsLoaded(options) {
|
||||
const themes = [
|
||||
{ val: 'white', title: 'White' },
|
||||
{ val: 'dark', title: 'Dark' },
|
||||
{ val: 'black', title: 'Black' }
|
||||
].concat(await server.get('options/user-themes'));
|
||||
|
||||
$themeSelect.empty();
|
||||
|
||||
for (const theme of themes) {
|
||||
$themeSelect.append($("<option>")
|
||||
.attr("value", theme.val)
|
||||
.attr("data-note-id", theme.noteId)
|
||||
.html(theme.title));
|
||||
}
|
||||
|
||||
$themeSelect.val(options.theme);
|
||||
|
||||
if (utils.isElectron()) {
|
||||
@@ -59,21 +78,35 @@ addTabHandler((function() {
|
||||
|
||||
$leftPaneMinWidth.val(options.leftPaneMinWidth);
|
||||
$leftPaneWidthPercent.val(options.leftPaneWidthPercent);
|
||||
|
||||
$mainFontSize.val(options.mainFontSize);
|
||||
$treeFontSize.val(options.treeFontSize);
|
||||
$detailFontSize.val(options.detailFontSize);
|
||||
}
|
||||
|
||||
$themeSelect.change(function() {
|
||||
const newTheme = $(this).val();
|
||||
|
||||
$html.attr("class", "theme-" + newTheme);
|
||||
for (const clazz of Array.from($body[0].classList)) { // create copy to safely iterate over while removing classes
|
||||
if (clazz.startsWith("theme-")) {
|
||||
$body.removeClass(clazz);
|
||||
}
|
||||
}
|
||||
|
||||
const noteId = $(this).find(":selected").attr("data-note-id");
|
||||
|
||||
if (noteId) {
|
||||
// make sure the CSS is loaded
|
||||
// if the CSS has been loaded and then updated then the changes won't take effect though
|
||||
cssLoader.requireCss(`/api/notes/download/${noteId}`);
|
||||
}
|
||||
|
||||
$body.addClass("theme-" + newTheme);
|
||||
|
||||
server.put('options/theme/' + newTheme);
|
||||
});
|
||||
|
||||
$zoomFactorSelect.change(function() {
|
||||
const newZoomFactor = $(this).val();
|
||||
|
||||
zoomService.setZoomFactorAndSave(newZoomFactor);
|
||||
});
|
||||
$zoomFactorSelect.change(function() { zoomService.setZoomFactorAndSave($(this).val()); });
|
||||
|
||||
function resizeLeftPanel() {
|
||||
const leftPanePercent = parseInt($leftPaneWidthPercent.val());
|
||||
@@ -83,20 +116,42 @@ addTabHandler((function() {
|
||||
$container.css("grid-template-columns", `minmax(${leftPaneMinWidth}px, ${leftPanePercent}fr) ${rightPanePercent}fr`);
|
||||
}
|
||||
|
||||
$leftPaneMinWidth.change(function() {
|
||||
const newMinWidth = $(this).val();
|
||||
$leftPaneMinWidth.change(async function() {
|
||||
await server.put('options/leftPaneMinWidth/' + $(this).val());
|
||||
|
||||
resizeLeftPanel();
|
||||
|
||||
server.put('options/leftPaneMinWidth/' + newMinWidth);
|
||||
});
|
||||
|
||||
$leftPaneWidthPercent.change(function() {
|
||||
const newWidthPercent = $(this).val();
|
||||
$leftPaneWidthPercent.change(async function() {
|
||||
await server.put('options/leftPaneWidthPercent/' + $(this).val());
|
||||
|
||||
resizeLeftPanel();
|
||||
});
|
||||
|
||||
server.put('options/leftPaneWidthPercent/' + newWidthPercent);
|
||||
function applyFontSizes() {
|
||||
console.log($mainFontSize.val() + "% !important");
|
||||
|
||||
$body.get(0).style.setProperty("--main-font-size", $mainFontSize.val() + "%");
|
||||
$body.get(0).style.setProperty("--tree-font-size", $treeFontSize.val() + "%");
|
||||
$body.get(0).style.setProperty("--detail-font-size", $detailFontSize.val() + "%");
|
||||
}
|
||||
|
||||
$mainFontSize.change(async function() {
|
||||
await server.put('options/mainFontSize/' + $(this).val());
|
||||
|
||||
applyFontSizes();
|
||||
});
|
||||
|
||||
$treeFontSize.change(async function() {
|
||||
await server.put('options/treeFontSize/' + $(this).val());
|
||||
|
||||
applyFontSizes();
|
||||
});
|
||||
|
||||
$detailFontSize.change(async function() {
|
||||
await server.put('options/detailFontSize/' + $(this).val());
|
||||
|
||||
applyFontSizes();
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -160,7 +160,7 @@ async function createPromotedAttributeRow(definitionAttr, valueAttr) {
|
||||
$input.autocomplete({
|
||||
appendTo: document.querySelector('body'),
|
||||
hint: false,
|
||||
autoselect: true,
|
||||
autoselect: false,
|
||||
openOnFocus: true,
|
||||
minLength: 0,
|
||||
tabAutocomplete: false
|
||||
|
||||
@@ -9,7 +9,7 @@ async function getAndExecuteBundle(noteId, originEntity = null) {
|
||||
}
|
||||
|
||||
async function executeBundle(bundle, originEntity) {
|
||||
const apiContext = ScriptContext(bundle.note, bundle.allNotes, originEntity);
|
||||
const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity);
|
||||
|
||||
try {
|
||||
return await (function () {
|
||||
@@ -30,9 +30,13 @@ async function executeStartupBundles() {
|
||||
}
|
||||
|
||||
async function executeRelationBundles(note, relationName) {
|
||||
const bundlesToRun = await server.get("script/relation/" + note.noteId + "/" + relationName);
|
||||
note.bundleCache = note.bundleCache || {};
|
||||
|
||||
for (const bundle of bundlesToRun) {
|
||||
if (!note.bundleCache[relationName]) {
|
||||
note.bundleCache[relationName] = await server.get("script/relation/" + note.noteId + "/" + relationName);
|
||||
}
|
||||
|
||||
for (const bundle of note.bundleCache[relationName]) {
|
||||
await executeBundle(bundle, note);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
async function requireCss(url) {
|
||||
const css = Array
|
||||
.from(document.querySelectorAll('link'))
|
||||
.map(scr => scr.href);
|
||||
|
||||
if (!css.includes(url)) {
|
||||
$('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url));
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
requireCss
|
||||
}
|
||||
@@ -9,6 +9,14 @@ const dragAndDropSetup = {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!data.originalEvent.ctrlKey) {
|
||||
// keep existing selection only if CTRL key is pressed
|
||||
for (const selectedNode of treeService.getSelectedNodes()) {
|
||||
selectedNode.setSelected(false);
|
||||
selectedNode.renderTitle();
|
||||
}
|
||||
}
|
||||
|
||||
node.setSelected(true);
|
||||
|
||||
// this is for dragging notes into relation map
|
||||
|
||||
@@ -61,8 +61,15 @@ function registerEntrypoints() {
|
||||
$("#history-back-button").click(window.history.back);
|
||||
$("#history-forward-button").click(window.history.forward);
|
||||
|
||||
utils.bindShortcut('alt+left', window.history.back);
|
||||
utils.bindShortcut('alt+right', window.history.forward);
|
||||
if (utils.isMac()) {
|
||||
// Mac has a different history navigation shortcuts - https://github.com/zadam/trilium/issues/376
|
||||
utils.bindShortcut('meta+left', window.history.back);
|
||||
utils.bindShortcut('meta+right', window.history.forward);
|
||||
}
|
||||
else {
|
||||
utils.bindShortcut('alt+left', window.history.back);
|
||||
utils.bindShortcut('alt+right', window.history.forward);
|
||||
}
|
||||
}
|
||||
|
||||
utils.bindShortcut('alt+m', e => {
|
||||
@@ -87,7 +94,7 @@ function registerEntrypoints() {
|
||||
|
||||
utils.bindShortcut('ctrl+r', utils.reloadApp);
|
||||
|
||||
$(document).bind('keydown', 'ctrl+shift+i', () => {
|
||||
utils.bindShortcut('ctrl+shift+i', () => {
|
||||
if (utils.isElectron()) {
|
||||
require('electron').remote.getCurrentWindow().toggleDevTools();
|
||||
|
||||
@@ -135,8 +142,8 @@ function registerEntrypoints() {
|
||||
});
|
||||
|
||||
if (utils.isElectron()) {
|
||||
$(document).bind('keydown', 'ctrl+-', zoomService.decreaseZoomFactor);
|
||||
$(document).bind('keydown', 'ctrl+=', zoomService.increaseZoomFactor);
|
||||
utils.bindShortcut('ctrl+-', zoomService.decreaseZoomFactor);
|
||||
utils.bindShortcut('ctrl+=', zoomService.increaseZoomFactor);
|
||||
}
|
||||
|
||||
$("#note-title").bind('keydown', 'return', () => $("#note-detail-text").focus());
|
||||
|
||||
@@ -23,6 +23,10 @@ $("#import-upload").change(async function() {
|
||||
const formData = new FormData();
|
||||
formData.append('upload', this.files[0]);
|
||||
|
||||
// this is done to reset the field otherwise triggering import same file again would not work
|
||||
// https://github.com/zadam/trilium/issues/388
|
||||
$("#import-upload").val('');
|
||||
|
||||
await $.ajax({
|
||||
url: baseApiUrl + 'notes/' + importNoteId + '/import',
|
||||
headers: server.getHeaders(),
|
||||
|
||||
@@ -10,6 +10,10 @@ $("#file-upload").change(async function() {
|
||||
const formData = new FormData();
|
||||
formData.append('upload', this.files[0]);
|
||||
|
||||
// this is done to reset the field otherwise triggering import same file again would not work
|
||||
// https://github.com/zadam/trilium/issues/388
|
||||
$("#file-upload").val('');
|
||||
|
||||
const resp = await $.ajax({
|
||||
url: baseApiUrl + 'notes/' + noteDetailService.getCurrentNoteId() + '/upload',
|
||||
headers: server.getHeaders(),
|
||||
|
||||
@@ -43,7 +43,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null) {
|
||||
this.activateNewNote = async notePath => {
|
||||
await treeService.reload();
|
||||
|
||||
await treeService.activateNote(notePath, noteDetailService.focusOnTitle);
|
||||
await treeService.activateNote(notePath, noteDetailService.focusAndSelectTitle);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import cssLoader from './css_loader.js';
|
||||
|
||||
const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
|
||||
|
||||
const CODE_MIRROR = {
|
||||
@@ -34,7 +36,7 @@ const RELATION_MAP = {
|
||||
|
||||
async function requireLibrary(library) {
|
||||
if (library.css) {
|
||||
library.css.map(cssUrl => requireCss(cssUrl));
|
||||
library.css.map(cssUrl => cssLoader.requireCss(cssUrl));
|
||||
}
|
||||
|
||||
if (library.js) {
|
||||
@@ -59,16 +61,6 @@ async function requireScript(url) {
|
||||
await loadedScriptPromises[url];
|
||||
}
|
||||
|
||||
async function requireCss(url) {
|
||||
const css = Array
|
||||
.from(document.querySelectorAll('link'))
|
||||
.map(scr => scr.href);
|
||||
|
||||
if (!css.includes(url)) {
|
||||
$('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url));
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
requireLibrary,
|
||||
CKEDITOR,
|
||||
|
||||
@@ -107,6 +107,7 @@ function init() {
|
||||
$(document).on('click', "a[data-action='note']", goToLink);
|
||||
$(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
|
||||
$(document).on('dblclick', '#note-detail-text a', goToLink);
|
||||
$(document).on('click', '#note-detail-text.ck-read-only a', goToLink);
|
||||
$(document).on('click', 'span.ck-button__label', e => {
|
||||
// this is a link preview dialog from CKEditor link editing
|
||||
// for some reason clicked element is span
|
||||
|
||||
@@ -30,6 +30,7 @@ const $noteIdDisplay = $("#note-id-display");
|
||||
const $childrenOverview = $("#children-overview");
|
||||
const $scriptArea = $("#note-detail-script-area");
|
||||
const $savedIndicator = $("#saved-indicator");
|
||||
const $body = $("body");
|
||||
|
||||
let currentNote = null;
|
||||
|
||||
@@ -131,6 +132,9 @@ async function saveNote() {
|
||||
}
|
||||
|
||||
$savedIndicator.fadeIn();
|
||||
|
||||
// run async
|
||||
bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteChange');
|
||||
}
|
||||
|
||||
async function saveNoteIfChanged() {
|
||||
@@ -142,11 +146,21 @@ async function saveNoteIfChanged() {
|
||||
$savedIndicator.fadeIn();
|
||||
}
|
||||
|
||||
function setNoteBackgroundIfProtected(note) {
|
||||
$noteDetailWrapper.toggleClass("protected", note.isProtected);
|
||||
$protectButton.toggleClass("active", note.isProtected);
|
||||
$unprotectButton.toggleClass("active", !note.isProtected);
|
||||
$unprotectButton.prop("disabled", !protectedSessionHolder.isProtectedSessionAvailable());
|
||||
function updateNoteView() {
|
||||
$noteDetailWrapper.toggleClass("protected", currentNote.isProtected);
|
||||
$protectButton.toggleClass("active", currentNote.isProtected);
|
||||
$protectButton.prop("disabled", currentNote.isProtected);
|
||||
$unprotectButton.toggleClass("active", !currentNote.isProtected);
|
||||
$unprotectButton.prop("disabled", !currentNote.isProtected || !protectedSessionHolder.isProtectedSessionAvailable());
|
||||
|
||||
for (const clazz of Array.from($body[0].classList)) { // create copy to safely iterate over while removing classes
|
||||
if (clazz.startsWith("type-") || clazz.startsWith("mime-")) {
|
||||
$body.removeClass(clazz);
|
||||
}
|
||||
}
|
||||
|
||||
$body.addClass(utils.getNoteTypeClass(currentNote.type));
|
||||
$body.addClass(utils.getMimeTypeClass(currentNote.mime));
|
||||
}
|
||||
|
||||
async function handleProtectedSession() {
|
||||
@@ -189,7 +203,7 @@ async function loadNoteDetail(noteId) {
|
||||
|
||||
$noteIdDisplay.html(noteId);
|
||||
|
||||
setNoteBackgroundIfProtected(currentNote);
|
||||
updateNoteView();
|
||||
|
||||
$noteDetailWrapper.show();
|
||||
|
||||
@@ -266,7 +280,7 @@ async function showChildrenOverview() {
|
||||
text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId)
|
||||
}).attr('data-action', 'note').attr('data-note-path', notePath + '/' + childBranch.noteId);
|
||||
|
||||
const childEl = $('<div class="child-overview">').html(link);
|
||||
const childEl = $('<div class="child-overview-item">').html(link);
|
||||
$childrenOverview.append(childEl);
|
||||
}
|
||||
|
||||
@@ -283,6 +297,10 @@ function focusOnTitle() {
|
||||
$noteTitle.focus();
|
||||
}
|
||||
|
||||
function focusAndSelectTitle() {
|
||||
$noteTitle.focus().select();
|
||||
}
|
||||
|
||||
/**
|
||||
* Since detail loading may take some time and user might just browse through the notes using UP-DOWN keys,
|
||||
* we intentionally decouple activation of the note in the tree and full load of the note so just avaiting on
|
||||
@@ -336,12 +354,13 @@ setInterval(saveNoteIfChanged, 3000);
|
||||
export default {
|
||||
reload,
|
||||
switchToNote,
|
||||
setNoteBackgroundIfProtected,
|
||||
updateNoteView,
|
||||
loadNote,
|
||||
getCurrentNote,
|
||||
getCurrentNoteType,
|
||||
getCurrentNoteId,
|
||||
focusOnTitle,
|
||||
focusAndSelectTitle,
|
||||
saveNote,
|
||||
saveNoteIfChanged,
|
||||
noteChanged,
|
||||
|
||||
@@ -5,6 +5,7 @@ import noteDetailService from "./note_detail.js";
|
||||
|
||||
const $component = $('#note-detail-file');
|
||||
|
||||
const $fileNoteId = $("#file-note-id");
|
||||
const $fileName = $("#file-filename");
|
||||
const $fileType = $("#file-filetype");
|
||||
const $fileSize = $("#file-filesize");
|
||||
@@ -21,6 +22,7 @@ async function show() {
|
||||
|
||||
$component.show();
|
||||
|
||||
$fileNoteId.text(currentNote.noteId);
|
||||
$fileName.text(attributeMap.originalFileName || "?");
|
||||
$fileSize.text((attributeMap.fileSize || "?") + " bytes");
|
||||
$fileType.text(currentNote.mime);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import libraryLoader from "./library_loader.js";
|
||||
import noteDetailService from './note_detail.js';
|
||||
import treeService from './tree.js';
|
||||
import attributeService from "./attributes.js";
|
||||
|
||||
const $component = $('#note-detail-text');
|
||||
|
||||
@@ -19,6 +20,8 @@ async function show() {
|
||||
}
|
||||
}
|
||||
|
||||
textEditor.isReadOnly = await isReadOnly();
|
||||
|
||||
textEditor.setData(noteDetailService.getCurrentNote().content);
|
||||
|
||||
$component.show();
|
||||
@@ -36,6 +39,12 @@ function getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
async function isReadOnly() {
|
||||
const attributes = await attributeService.getAttributes();
|
||||
|
||||
return attributes.some(attr => attr.type === 'label' && attr.name === 'readOnly');
|
||||
}
|
||||
|
||||
function focus() {
|
||||
$component.focus();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import treeService from './tree.js';
|
||||
import noteDetailService from './note_detail.js';
|
||||
import server from './server.js';
|
||||
import infoService from "./info.js";
|
||||
import confirmDialog from "../dialogs/confirm.js";
|
||||
|
||||
const $executeScriptButton = $("#execute-script-button");
|
||||
const $toggleEditButton = $('#toggle-edit-button');
|
||||
@@ -110,35 +111,63 @@ function NoteTypeModel() {
|
||||
self.updateExecuteScriptButtonVisibility();
|
||||
}
|
||||
|
||||
this.selectText = function() {
|
||||
function confirmChangeIfContent() {
|
||||
if (!noteDetailService.getCurrentNoteContent()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return confirmDialog.confirm("It is not recommended to change note type when note content is not empty. Do you want to continue anyway?");
|
||||
}
|
||||
|
||||
this.selectText = async function() {
|
||||
if (!await confirmChangeIfContent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.type('text');
|
||||
self.mime('');
|
||||
self.mime('text/html');
|
||||
|
||||
save();
|
||||
};
|
||||
|
||||
this.selectRender = function() {
|
||||
this.selectRender = async function() {
|
||||
if (!await confirmChangeIfContent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.type('render');
|
||||
self.mime('text/html');
|
||||
|
||||
save();
|
||||
};
|
||||
|
||||
this.selectRelationMap = function() {
|
||||
this.selectRelationMap = async function() {
|
||||
if (!await confirmChangeIfContent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.type('relation-map');
|
||||
self.mime('application/json');
|
||||
|
||||
save();
|
||||
};
|
||||
|
||||
this.selectCode = function() {
|
||||
this.selectCode = async function() {
|
||||
if (!await confirmChangeIfContent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.type('code');
|
||||
self.mime('');
|
||||
self.mime('text/plain');
|
||||
|
||||
save();
|
||||
};
|
||||
|
||||
this.selectCodeMime = function(el) {
|
||||
this.selectCodeMime = async function(el) {
|
||||
if (!await confirmChangeIfContent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.type('code');
|
||||
self.mime(el.mime);
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ async function protectNoteAndSendToServer() {
|
||||
|
||||
treeService.setProtected(note.noteId, note.isProtected);
|
||||
|
||||
noteDetailService.setNoteBackgroundIfProtected(note);
|
||||
noteDetailService.updateNoteView();
|
||||
}
|
||||
|
||||
async function unprotectNoteAndSendToServer() {
|
||||
@@ -152,7 +152,7 @@ async function unprotectNoteAndSendToServer() {
|
||||
|
||||
treeService.setProtected(currentNote.noteId, currentNote.isProtected);
|
||||
|
||||
noteDetailService.setNoteBackgroundIfProtected(currentNote);
|
||||
noteDetailService.updateNoteView();
|
||||
}
|
||||
|
||||
async function protectSubtree(noteId, protect) {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import FrontendScriptApi from './frontend_script_api.js';
|
||||
import utils from './utils.js';
|
||||
import treeCache from './tree_cache.js';
|
||||
|
||||
function ScriptContext(startNote, allNotes, originEntity = null) {
|
||||
async function ScriptContext(startNoteId, allNoteIds, originEntity = null) {
|
||||
const modules = {};
|
||||
|
||||
const startNote = await treeCache.getNote(startNoteId);
|
||||
const allNotes = await treeCache.getNotes(allNoteIds);
|
||||
|
||||
return {
|
||||
modules: modules,
|
||||
notes: utils.toObject(allNotes, note => [note.noteId, note]),
|
||||
|
||||
@@ -76,6 +76,15 @@ async function saveSearch() {
|
||||
await treeService.activateNote(noteId);
|
||||
}
|
||||
|
||||
function init() {
|
||||
const hashValue = treeService.getHashValueFromAddress();
|
||||
|
||||
if (hashValue.startsWith("search=")) {
|
||||
showSearch();
|
||||
doSearch(hashValue.substr(7));
|
||||
}
|
||||
}
|
||||
|
||||
$searchInput.keyup(e => {
|
||||
const searchText = $searchInput.val();
|
||||
|
||||
@@ -100,5 +109,6 @@ export default {
|
||||
toggleSearch,
|
||||
resetSearch,
|
||||
showSearch,
|
||||
doSearch
|
||||
doSearch,
|
||||
init
|
||||
};
|
||||
@@ -483,24 +483,30 @@ async function reload() {
|
||||
await getTree().reload(notes);
|
||||
}
|
||||
|
||||
function getNotePathFromAddress() {
|
||||
return document.location.hash.substr(1); // strip initial #
|
||||
function isNotePathInAddress() {
|
||||
return getHashValueFromAddress().startsWith("root");
|
||||
}
|
||||
|
||||
function getHashValueFromAddress() {
|
||||
return document.location.hash ? document.location.hash.substr(1) : ""; // strip initial #
|
||||
}
|
||||
|
||||
async function loadTree() {
|
||||
const resp = await server.get('tree');
|
||||
startNotePath = resp.startNotePath;
|
||||
|
||||
if (document.location.hash) {
|
||||
startNotePath = getNotePathFromAddress();
|
||||
if (isNotePathInAddress()) {
|
||||
startNotePath = getHashValueFromAddress();
|
||||
}
|
||||
|
||||
return await treeBuilder.prepareTree(resp.notes, resp.branches, resp.relations);
|
||||
}
|
||||
|
||||
function collapseTree(node = null) {
|
||||
async function collapseTree(node = null) {
|
||||
if (!node) {
|
||||
node = $tree.fancytree("getRootNode");
|
||||
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
|
||||
|
||||
node = getNodesByNoteId(hoistedNoteId)[0];
|
||||
}
|
||||
|
||||
node.setExpanded(false);
|
||||
@@ -541,9 +547,11 @@ async function setNoteTitle(noteId, title) {
|
||||
}
|
||||
|
||||
async function createNewTopLevelNote() {
|
||||
const rootNode = getNodesByNoteId('root')[0];
|
||||
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
|
||||
|
||||
await createNote(rootNode, "root", "into", false);
|
||||
const rootNode = getNodesByNoteId(hoistedNoteId)[0];
|
||||
|
||||
await createNote(rootNode, hoistedNoteId, "into", false);
|
||||
}
|
||||
|
||||
async function createNote(node, parentNoteId, target, isProtected, saveSelection = false) {
|
||||
@@ -588,7 +596,7 @@ async function createNote(node, parentNoteId, target, isProtected, saveSelection
|
||||
|
||||
await noteDetailService.saveNoteIfChanged();
|
||||
|
||||
noteDetailService.addDetailLoadedListener(note.noteId, noteDetailService.focusOnTitle);
|
||||
noteDetailService.addDetailLoadedListener(note.noteId, noteDetailService.focusAndSelectTitle);
|
||||
|
||||
const noteEntity = new NoteShort(treeCache, note);
|
||||
const branchEntity = new Branch(treeCache, branch);
|
||||
@@ -619,7 +627,10 @@ async function createNote(node, parentNoteId, target, isProtected, saveSelection
|
||||
|
||||
await node.getLastChild().setActive(true);
|
||||
|
||||
const parentNoteEntity = await treeCache.getNote(node.data.noteId);
|
||||
|
||||
node.folder = true;
|
||||
node.icon = await treeBuilder.getIcon(parentNoteEntity); // icon might change into folder
|
||||
node.renderTitle();
|
||||
}
|
||||
else {
|
||||
@@ -700,12 +711,14 @@ utils.bindShortcut('ctrl+p', createNoteInto);
|
||||
utils.bindShortcut('ctrl+.', scrollToCurrentNote);
|
||||
|
||||
$(window).bind('hashchange', function() {
|
||||
const notePath = getNotePathFromAddress();
|
||||
if (isNotePathInAddress()) {
|
||||
const notePath = getHashValueFromAddress();
|
||||
|
||||
if (notePath !== '-' && getCurrentNotePath() !== notePath) {
|
||||
console.debug("Switching to " + notePath + " because of hash change");
|
||||
if (notePath !== '-' && getCurrentNotePath() !== notePath) {
|
||||
console.debug("Switching to " + notePath + " because of hash change");
|
||||
|
||||
activateNote(notePath);
|
||||
activateNote(notePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -738,5 +751,6 @@ export default {
|
||||
showTree,
|
||||
loadTree,
|
||||
treeInitialized,
|
||||
setExpandedToServer
|
||||
setExpandedToServer,
|
||||
getHashValueFromAddress
|
||||
};
|
||||
@@ -166,7 +166,11 @@ async function getExtraClasses(note) {
|
||||
extraClasses.push(note.cssClass);
|
||||
}
|
||||
|
||||
extraClasses.push(note.type);
|
||||
extraClasses.push(utils.getNoteTypeClass(note.type));
|
||||
|
||||
if (note.mime) { // some notes should not have mime type (e.g. render)
|
||||
extraClasses.push(utils.getMimeTypeClass(note.mime));
|
||||
}
|
||||
|
||||
return extraClasses.join(" ");
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ function formatTimeWithSeconds(date) {
|
||||
return padNum(date.getHours()) + ":" + padNum(date.getMinutes()) + ":" + padNum(date.getSeconds());
|
||||
}
|
||||
|
||||
// this is producing local time!
|
||||
function formatDate(date) {
|
||||
// return padNum(date.getDate()) + ". " + padNum(date.getMonth() + 1) + ". " + date.getFullYear();
|
||||
// instead of european format we'll just use ISO as that's pretty unambiguous
|
||||
@@ -30,6 +31,7 @@ function formatDate(date) {
|
||||
return formatDateISO(date);
|
||||
}
|
||||
|
||||
// this is producing local time!
|
||||
function formatDateISO(date) {
|
||||
return date.getFullYear() + "-" + padNum(date.getMonth() + 1) + "-" + padNum(date.getDate());
|
||||
}
|
||||
@@ -143,6 +145,7 @@ function bindShortcut(keyboardShortcut, handler) {
|
||||
}
|
||||
|
||||
$(document).bind('keydown', keyboardShortcut, e => {
|
||||
console.log(e);
|
||||
handler();
|
||||
|
||||
e.preventDefault();
|
||||
@@ -151,11 +154,15 @@ function bindShortcut(keyboardShortcut, handler) {
|
||||
}
|
||||
|
||||
function isMobile() {
|
||||
return window.device === "mobile";
|
||||
return window.device === "mobile"
|
||||
// window.device is not available in setup
|
||||
|| (!window.device && /Mobi/.test(navigator.userAgent));
|
||||
}
|
||||
|
||||
function isDesktop() {
|
||||
return window.device === "desktop";
|
||||
return window.device === "desktop"
|
||||
// window.device is not available in setup
|
||||
|| (!window.device && !/Mobi/.test(navigator.userAgent));
|
||||
}
|
||||
|
||||
function setCookie(name, value) {
|
||||
@@ -165,6 +172,21 @@ function setCookie(name, value) {
|
||||
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
||||
}
|
||||
|
||||
function getNoteTypeClass(type) {
|
||||
return "type-" + type;
|
||||
}
|
||||
|
||||
function getMimeTypeClass(mime) {
|
||||
const semicolonIdx = mime.indexOf(';');
|
||||
|
||||
if (semicolonIdx !== -1) {
|
||||
// stripping everything following the semicolon
|
||||
mime = mime.substr(0, semicolonIdx);
|
||||
}
|
||||
|
||||
return 'mime-' + mime.toLowerCase().replace(/[\W_]+/g,"-");
|
||||
}
|
||||
|
||||
export default {
|
||||
reloadApp,
|
||||
parseDate,
|
||||
@@ -191,5 +213,7 @@ export default {
|
||||
bindShortcut,
|
||||
isMobile,
|
||||
isDesktop,
|
||||
setCookie
|
||||
setCookie,
|
||||
getNoteTypeClass,
|
||||
getMimeTypeClass
|
||||
};
|
||||
+2
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -9,12 +9,12 @@
|
||||
*
|
||||
* This section is automatically generated from the `skin-common.less` template.
|
||||
*
|
||||
* Copyright (c) 2008-2018, Martin Wendt (http://wwWendt.de)
|
||||
* Copyright (c) 2008-2019, Martin Wendt (http://wwWendt.de)
|
||||
* Released under the MIT license
|
||||
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
||||
*
|
||||
* @version 2.30.0
|
||||
* @date 2018-09-02T15:42:49Z
|
||||
* @version 2.30.2
|
||||
* @date 2019-01-13T08:17:01Z
|
||||
******************************************************************************/
|
||||
/*------------------------------------------------------------------------------
|
||||
* Helpers
|
||||
@@ -336,7 +336,8 @@ span.fancytree-icon {
|
||||
.fancytree-loading span.fancytree-expander,
|
||||
.fancytree-loading span.fancytree-expander:hover,
|
||||
.fancytree-statusnode-loading span.fancytree-icon,
|
||||
.fancytree-statusnode-loading span.fancytree-icon:hover {
|
||||
.fancytree-statusnode-loading span.fancytree-icon:hover,
|
||||
span.fancytree-icon.fancytree-icon-loading {
|
||||
background-image: url("../skin-win8/loading.gif");
|
||||
background-position: 0px 0px;
|
||||
}
|
||||
@@ -479,6 +480,8 @@ ul.fancytree-container.fancytree-rtl.fancytree-no-connector > li {
|
||||
* 'table' extension
|
||||
*----------------------------------------------------------------------------*/
|
||||
table.fancytree-ext-table {
|
||||
font-family: tahoma, arial, helvetica;
|
||||
font-size: 10pt;
|
||||
border-collapse: collapse;
|
||||
/* ext-ariagrid */
|
||||
}
|
||||
@@ -536,6 +539,9 @@ table.fancytree-ext-columnview span.fancytree-node {
|
||||
display: inline-block;
|
||||
}
|
||||
table.fancytree-ext-columnview span.fancytree-node.fancytree-expanded {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
table.fancytree-ext-columnview span.fancytree-node.fancytree-active {
|
||||
background-color: #CBE8F6;
|
||||
}
|
||||
table.fancytree-ext-columnview .fancytree-has-children span.fancytree-cv-right {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,7 @@
|
||||
body {
|
||||
font-size: var(--main-font-size);
|
||||
}
|
||||
|
||||
#container {
|
||||
margin: 0 auto; /* center */
|
||||
height: 100vh;
|
||||
@@ -26,6 +30,8 @@
|
||||
flex-shrink: 1;
|
||||
flex-basis: 60%;
|
||||
margin-top: 10px;
|
||||
font-family: var(--tree-font-family);
|
||||
font-size: var(--tree-font-size);
|
||||
}
|
||||
|
||||
#left-pane {
|
||||
@@ -36,7 +42,7 @@
|
||||
|
||||
#header {
|
||||
grid-area: header;
|
||||
background-color: #f8f8f8;
|
||||
background-color: var(--header-background-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
@@ -44,7 +50,7 @@
|
||||
|
||||
#header button {
|
||||
padding: 1px 5px 1px 5px;
|
||||
font-size: small;
|
||||
font-size: smaller;
|
||||
margin-bottom: 2px;
|
||||
margin-top: 2px;
|
||||
margin-right: 8px;
|
||||
@@ -61,7 +67,7 @@
|
||||
justify-content: space-around;
|
||||
padding: 10px 0 10px 0;
|
||||
margin: 0 20px 0 10px;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
@@ -85,4 +91,17 @@
|
||||
margin-top: 2px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
#note-detail-wrapper {
|
||||
font-size: var(--detail-font-size);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
@@ -24,7 +24,6 @@ html, body {
|
||||
}
|
||||
|
||||
#tree {
|
||||
font-size: larger;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -45,7 +44,8 @@ html, body {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
/* for some reason detail overflows a little bit so we subtract few pixels */
|
||||
height: calc(100% - 25px);
|
||||
/* large left padding is necessary for ckeditor gutter in detail-only (smartphone) layout */
|
||||
padding-left: 35px;
|
||||
}
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
.note-box {
|
||||
padding: 16px;
|
||||
position: absolute !important;
|
||||
background-color: var(--accented-background-color);
|
||||
color: var(--main-text-color);
|
||||
z-index: 4;
|
||||
border: 1px solid #666;
|
||||
box-shadow: 2px 2px 19px #999;
|
||||
border-radius: 8px;
|
||||
opacity: 0.8;
|
||||
background-color: white;
|
||||
font-size: 11px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
@@ -28,7 +29,7 @@
|
||||
}
|
||||
|
||||
.note-box:hover {
|
||||
background-color: #ddd;
|
||||
background-color: var(--more-accented-background-color);
|
||||
}
|
||||
|
||||
.note-box .title {
|
||||
@@ -37,11 +38,12 @@
|
||||
}
|
||||
|
||||
.connection-label.jtk-hover, .jtk-source-hover, .jtk-target-hover {
|
||||
background-color: #ddd;
|
||||
background-color: var(--more-accented-background-color);
|
||||
}
|
||||
|
||||
.connection-label {
|
||||
background-color: white;
|
||||
background-color: var(--accented-background-color);
|
||||
color: var(--main-text-color);
|
||||
opacity: 0.8;
|
||||
padding: 0.3em;
|
||||
border-radius: 0.5em;
|
||||
@@ -64,10 +66,6 @@
|
||||
box-shadow: 0 0 6px black;
|
||||
}
|
||||
|
||||
.statemachine-demo .jtk-endpoint {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.dragHover {
|
||||
border: 2px solid orange;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,125 @@
|
||||
:root {
|
||||
--main-font-family: inherit;
|
||||
--main-font-size: normal;
|
||||
--tree-font-family: inherit;
|
||||
--tree-font-size: normal;
|
||||
--detail-font-family: inherit;
|
||||
--detail-font-size: normal;
|
||||
--detail-text-font-family: inherit;
|
||||
|
||||
--main-background-color: white;
|
||||
--main-text-color: black;
|
||||
--main-border-color: #ccc;
|
||||
--accented-background-color: #eee;
|
||||
--more-accented-background-color: #ccc;
|
||||
--header-background-color: #f8f8f8;
|
||||
--button-background-color: #eee;
|
||||
--button-disabled-background-color: #ccc;
|
||||
--button-border-color: #ddd;
|
||||
--button-text-color: black;
|
||||
--button-border-radius: 5px;
|
||||
--muted-text-color: #444;
|
||||
--input-text-color: black;
|
||||
--input-background-color: white;
|
||||
--modal-background-color: white;
|
||||
--hover-item-text-color: black;
|
||||
--hover-item-background-color: #eee;
|
||||
--active-item-text-color: black;
|
||||
--active-item-background-color: #ccc;
|
||||
--menu-text-color: black;
|
||||
--menu-background-color: white;
|
||||
}
|
||||
|
||||
body.theme-black {
|
||||
--main-background-color: black;
|
||||
--main-text-color: white;
|
||||
--main-border-color: #ddd;
|
||||
--accented-background-color: #222;
|
||||
--more-accented-background-color: #444;
|
||||
--header-background-color: black;
|
||||
--button-background-color: #333;
|
||||
--button-border-color: #444;
|
||||
--button-text-color: white;
|
||||
--button-border-radius: 5px;
|
||||
--muted-text-color: #ccc;
|
||||
--input-text-color: white;
|
||||
--input-background-color: black;
|
||||
--modal-background-color: #222;
|
||||
--hover-item-text-color: black;
|
||||
--hover-item-background-color: #aaa;
|
||||
--active-item-text-color: black;
|
||||
--active-item-background-color: #ccc;
|
||||
--menu-text-color: white;
|
||||
--menu-background-color: #222;
|
||||
}
|
||||
|
||||
body.theme-black .CodeMirror {
|
||||
filter: invert(100%) hue-rotate(180deg);
|
||||
}
|
||||
|
||||
body.theme-dark {
|
||||
--main-background-color: #333;
|
||||
--main-text-color: white;
|
||||
--main-border-color: #ddd;
|
||||
--accented-background-color: #555;
|
||||
--more-accented-background-color: #777;
|
||||
--header-background-color: #333;
|
||||
--button-background-color: #555;
|
||||
--button-border-color: #444;
|
||||
--button-text-color: white;
|
||||
--button-border-radius: 5px;
|
||||
--muted-text-color: #ccc;
|
||||
--input-text-color: white;
|
||||
--input-background-color: #333;
|
||||
--modal-background-color: #555;
|
||||
--hover-item-text-color: black;
|
||||
--hover-item-background-color: #aaa;
|
||||
--active-item-text-color: black;
|
||||
--active-item-background-color: #ccc;
|
||||
--menu-text-color: white;
|
||||
--menu-background-color: #222;
|
||||
}
|
||||
|
||||
body.theme-dark .CodeMirror {
|
||||
filter: invert(90%) hue-rotate(180deg);
|
||||
}
|
||||
|
||||
html {
|
||||
/* this fixes FF filter vs. position fixed bug: https://github.com/zadam/trilium/issues/233 */
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
/* Fix for CKEditor block gutter icon "stretching" body and causing scrollbar to appear after pressing enter
|
||||
on the last line of the editor. */
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
background-color: var(--main-background-color);
|
||||
color: var(--main-text-color);
|
||||
font-family: var(--main-font-family);
|
||||
}
|
||||
|
||||
input, select {
|
||||
color: var(--input-text-color) !important;
|
||||
background: var(--input-background-color) !important;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background-color: var(--accented-background-color) !important;
|
||||
color: var(--muted-text-color) !important;
|
||||
}
|
||||
|
||||
button.close {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--modal-background-color) !important;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background-color: var(--more-accented-background-color) !important;
|
||||
color: var(--main-text-color) !important;
|
||||
}
|
||||
|
||||
#title-container {
|
||||
@@ -11,7 +128,8 @@ body {
|
||||
|
||||
#note-title {
|
||||
margin-left: 15px;
|
||||
font-size: x-large;
|
||||
margin-right: 10px;
|
||||
font-size: 150%;
|
||||
border: 0;
|
||||
width: 5em;
|
||||
flex-grow: 100;
|
||||
@@ -48,7 +166,7 @@ ul.fancytree-container {
|
||||
|
||||
/* this is done to preserve correct indentation. Better solution would be preferable */
|
||||
.fancytree-node:not(.fancytree-folder) .fancytree-expander:before {
|
||||
color: white;
|
||||
color: var(--main-background-color); /* setting to background color makes this invisible */
|
||||
}
|
||||
|
||||
.fancytree-node.fancytree-expanded .fancytree-expander:before {
|
||||
@@ -71,6 +189,7 @@ ul.fancytree-container {
|
||||
padding-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: var(--detail-font-family);
|
||||
}
|
||||
|
||||
#note-detail-component-wrapper {
|
||||
@@ -89,8 +208,6 @@ ul.fancytree-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#note-detail-text { font-size: 1.1em; }
|
||||
|
||||
#note-detail-text h1 { font-size: 2.0em; }
|
||||
#note-detail-text h2 { font-size: 1.8em; }
|
||||
#note-detail-text h3 { font-size: 1.6em; }
|
||||
@@ -104,6 +221,11 @@ ul.fancytree-container {
|
||||
/* This is because with empty content height of editor is 0 and it's impossible to click into it */
|
||||
min-height: 200px;
|
||||
overflow: auto;
|
||||
font-family: var(--detail-text-font-family);
|
||||
}
|
||||
|
||||
#note-detail-text p:first-child, #note-detail-text::before {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/** we disable shield background when in distraction free mode because I couldn't get it to stay static
|
||||
@@ -117,12 +239,18 @@ ul.fancytree-container {
|
||||
|
||||
ul.fancytree-container {
|
||||
outline: none !important;
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
.fancytree-custom-icon {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
span.fancytree-title {
|
||||
color: inherit !important;
|
||||
background: inherit !important;
|
||||
}
|
||||
|
||||
span.fancytree-node.protected > span.fancytree-custom-icon {
|
||||
filter: drop-shadow(2px 2px 2px black);
|
||||
}
|
||||
@@ -140,22 +268,34 @@ span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-tit
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
/* By default not focused active tree item is not easily visible, this makes it more visible */
|
||||
span.fancytree-active:not(.fancytree-focused) .fancytree-title {
|
||||
background-color: #eee !important;
|
||||
border-color: #ddd !important;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
span.fancytree-active.fancytree-focused .fancytree-title {
|
||||
background-color: #ddd !important;
|
||||
border-color: #bbb !important;
|
||||
color: var(--active-item-text-color) !important;
|
||||
background-color: var(--active-item-background-color) !important;
|
||||
border-color: var(--main-border-color) !important;
|
||||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.fancytree-plain span.fancytree-node:hover span.fancytree-title {
|
||||
background-color: #eee !important;
|
||||
border-color: #bbb !important;
|
||||
span.fancytree-active:not(.fancytree-focused) .fancytree-title {
|
||||
color: var(--hover-item-text-color) !important;
|
||||
background-color: var(--hover-item-background-color) !important;
|
||||
border-color: var(--main-border-color) !important;
|
||||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
span.fancytree-selected:not(.fancytree-active) .fancytree-title {
|
||||
color: var(--hover-item-text-color) !important;
|
||||
background-color: var(--hover-item-background-color) !important;
|
||||
border-color: var(--main-border-color) !important;
|
||||
border-radius: 3px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
span.fancytree-node:not(.fancytree-active):hover span.fancytree-title {
|
||||
color: var(--hover-item-text-color) !important;
|
||||
background-color: var(--hover-item-background-color) !important;
|
||||
border-color: var(--main-border-color) !important;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@@ -198,13 +338,17 @@ div.ui-tooltip {
|
||||
margin-top: 10px;
|
||||
display: none;
|
||||
overflow: auto;
|
||||
border-bottom: 2px solid #ddd;
|
||||
border-bottom: 2px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
#search-results ul {
|
||||
padding: 5px 5px 5px 15px;
|
||||
}
|
||||
|
||||
#search-text {
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
/*
|
||||
* .electron-in-page-search-window is a class specified to default
|
||||
* <webview> element for search window.
|
||||
@@ -244,7 +388,7 @@ div.ui-tooltip {
|
||||
|
||||
/* Allow to use <kbd> elements inside the title to define shortcut hints. */
|
||||
.ui-menu kbd, button kbd {
|
||||
color: black;
|
||||
color: var(--muted-text-color);
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
@@ -265,8 +409,14 @@ div.ui-tooltip {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
color: var(--menu-text-color) !important;
|
||||
background-color: var(--menu-background-color) !important;
|
||||
}
|
||||
|
||||
.dropdown-menu a:hover:not(.disabled) {
|
||||
background-color: #eee !important;
|
||||
color: var(--hover-item-text-color) !important;
|
||||
background-color: var(--hover-item-background-color) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -276,7 +426,7 @@ div.ui-tooltip {
|
||||
|
||||
.dropdown-menu kbd
|
||||
{
|
||||
color: black;
|
||||
color: var(--muted-text-color);
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
@@ -309,6 +459,11 @@ div.ui-tooltip {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.CodeMirror-gutters {
|
||||
background-color: inherit !important;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
#note-id-display {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
@@ -329,10 +484,12 @@ div.ui-tooltip {
|
||||
overflow: auto;
|
||||
/* limiting the size since actual note content is more important */
|
||||
max-height: 30%;
|
||||
flex-shrink: 0;
|
||||
flex-basis: 2em;
|
||||
}
|
||||
|
||||
#label-list, #relation-list, #attribute-list {
|
||||
color: #777777;
|
||||
color: var(--muted-text-color);
|
||||
padding: 5px;
|
||||
display: none;
|
||||
}
|
||||
@@ -344,7 +501,7 @@ div.ui-tooltip {
|
||||
|
||||
#file-table th, #file-table td {
|
||||
padding: 10px;
|
||||
font-size: large;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
#children-overview {
|
||||
@@ -358,11 +515,10 @@ div.ui-tooltip {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.child-overview {
|
||||
.child-overview-item {
|
||||
font-weight: bold;
|
||||
font-size: large;
|
||||
padding: 10px;
|
||||
background: #f4f4f4;
|
||||
background: var(--accented-background-color);
|
||||
width: 150px;
|
||||
height: 90px;
|
||||
line-height: 2em;
|
||||
@@ -375,8 +531,8 @@ div.ui-tooltip {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.child-overview a {
|
||||
color: #444;
|
||||
.child-overview-item a {
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
#sql-console-query {
|
||||
@@ -390,13 +546,18 @@ div.ui-tooltip {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: var(--button-border-radius);
|
||||
}
|
||||
|
||||
.btn:not(.btn-primary):not(.btn-secondary):not(.btn-danger) {
|
||||
border-color: #ddd;
|
||||
background-color: #eee;
|
||||
border-color: var(--button-border-color);
|
||||
background-color: var(--button-background-color);
|
||||
color: var(--button-text-color);
|
||||
}
|
||||
|
||||
.btn.active:not(.btn-primary) {
|
||||
background-color: #ccc;
|
||||
background-color: var(--button-disabled-background-color) !important;
|
||||
}
|
||||
|
||||
#note-path-list a.current {
|
||||
@@ -417,33 +578,6 @@ button.icon-button {
|
||||
width: 15em;
|
||||
}
|
||||
|
||||
/* Themes */
|
||||
|
||||
html {
|
||||
/* this fixes FF filter vs. position fixed bug: https://github.com/zadam/trilium/issues/233 */
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html.theme-black, html.theme-black img, html.theme-black video {
|
||||
filter: invert(100%) hue-rotate(180deg);
|
||||
}
|
||||
|
||||
html.theme-black body {
|
||||
background: black;
|
||||
}
|
||||
|
||||
html.theme-dark {
|
||||
filter: invert(90%) hue-rotate(180deg);
|
||||
}
|
||||
|
||||
html.theme-dark img, html.theme-dark video {
|
||||
filter: invert(100%) hue-rotate(180deg);
|
||||
}
|
||||
|
||||
html.theme-dark body {
|
||||
background: #191819;
|
||||
}
|
||||
|
||||
.ck.ck-block-toolbar-button {
|
||||
transform: translateX(10px);
|
||||
}
|
||||
@@ -470,8 +604,12 @@ html.theme-dark body {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
pre:not(.CodeMirror-line) {
|
||||
color: var(--main-text-color) !important;
|
||||
}
|
||||
|
||||
#file-preview-content {
|
||||
background-color: #f6f6f6;
|
||||
background-color: var(--accented-background-color);
|
||||
padding: 15px;
|
||||
max-width: 600px;
|
||||
max-height: 300px;
|
||||
@@ -513,7 +651,7 @@ html.theme-dark body {
|
||||
|
||||
.go-to-selected-note-button.disabled, .go-to-selected-note-button.disabled:hover {
|
||||
cursor: inherit;
|
||||
color: #ccc !important;
|
||||
color: var(--button-disabled-background-color) !important;
|
||||
}
|
||||
|
||||
.note-autocomplete-input {
|
||||
@@ -546,13 +684,13 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
|
||||
}
|
||||
|
||||
.tooltip-inner {
|
||||
background-color: #fbfbfb !important;
|
||||
background-color: var(--accented-background-color) !important;
|
||||
max-width: 400px;
|
||||
/* height needs to stay small because tooltip has problem when it can't fit to either top or bottom of the cursor */
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
color: black;
|
||||
border: 1px solid #ccc;
|
||||
color: var(--main-text-color);
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 5px;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -581,8 +719,8 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
|
||||
|
||||
.algolia-autocomplete .aa-dropdown-menu {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border: 1px solid #999;
|
||||
background-color: var(--main-background-color);
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-top: none;
|
||||
z-index: 2000 !important;
|
||||
max-height: 500px;
|
||||
@@ -603,7 +741,8 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
|
||||
}
|
||||
|
||||
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion.aa-cursor {
|
||||
background-color: #B2D7FF;
|
||||
color: var(--hover-item-text-color);
|
||||
background-color: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
.help-button {
|
||||
@@ -668,9 +807,9 @@ div[data-notify="container"] {
|
||||
#saved-indicator {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 11px;
|
||||
font-size: x-large;
|
||||
color: #777;
|
||||
top: -7px;
|
||||
font-size: 150%;
|
||||
color: var(--main-text-color);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@@ -707,7 +846,7 @@ div[data-notify="container"] {
|
||||
|
||||
margin: var(--ck-spacing-large) 0;
|
||||
|
||||
color: #aaa;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.fancytree-loading span.fancytree-expander {
|
||||
@@ -730,4 +869,13 @@ div[data-notify="container"] {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.ck-content .image > figcaption {
|
||||
color: var(--main-text-color);
|
||||
background-color: var(--accented-background-color);
|
||||
}
|
||||
|
||||
#options-dialog input[type=number] {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
const noteService = require('../../services/notes');
|
||||
const protectedSessionService = require('../../services/protected_session');
|
||||
const repository = require('../../services/repository');
|
||||
const utils = require('../../services/utils');
|
||||
|
||||
async function uploadFile(req) {
|
||||
const parentNoteId = req.params.parentNoteId;
|
||||
@@ -33,8 +34,7 @@ async function uploadFile(req) {
|
||||
};
|
||||
}
|
||||
|
||||
async function downloadFile(req, res) {
|
||||
const noteId = req.params.noteId;
|
||||
async function downloadNoteFile(noteId, res) {
|
||||
const note = await repository.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
@@ -42,20 +42,27 @@ async function downloadFile(req, res) {
|
||||
}
|
||||
|
||||
if (note.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
|
||||
res.status(401).send("Protected session not available");
|
||||
return;
|
||||
return res.status(401).send("Protected session not available");
|
||||
}
|
||||
|
||||
const originalFileName = await note.getLabel('originalFileName');
|
||||
const fileName = originalFileName ? originalFileName.value : note.title;
|
||||
|
||||
res.setHeader('Content-Disposition', 'file; filename="' + fileName + '"');
|
||||
res.setHeader('Content-Disposition', utils.getContentDisposition(fileName));
|
||||
res.setHeader('Content-Type', note.mime);
|
||||
|
||||
res.send(note.content);
|
||||
}
|
||||
|
||||
async function downloadFile(req, res) {
|
||||
const noteId = req.params.noteId;
|
||||
|
||||
return await downloadNoteFile(noteId, res);
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
uploadFile,
|
||||
downloadFile
|
||||
downloadFile,
|
||||
downloadNoteFile
|
||||
};
|
||||
@@ -1,12 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
const sql = require('../../services/sql');
|
||||
const optionService = require('../../services/options');
|
||||
const log = require('../../services/log');
|
||||
const attributes = require('../../services/attributes');
|
||||
|
||||
// options allowed to be updated directly in options dialog
|
||||
const ALLOWED_OPTIONS = ['protectedSessionTimeout', 'noteRevisionSnapshotTimeInterval',
|
||||
'zoomFactor', 'theme', 'syncServerHost', 'syncServerTimeout', 'syncProxy', 'leftPaneMinWidth', 'leftPaneWidthPercent', 'hoistedNoteId'];
|
||||
'zoomFactor', 'theme', 'syncServerHost', 'syncServerTimeout', 'syncProxy', 'leftPaneMinWidth', 'leftPaneWidthPercent', 'hoistedNoteId', 'mainFontSize', 'treeFontSize', 'detailFontSize'];
|
||||
|
||||
async function getOptions() {
|
||||
return await optionService.getOptionsMap(ALLOWED_OPTIONS);
|
||||
@@ -42,8 +42,31 @@ async function update(name, value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function getUserThemes() {
|
||||
const notes = await attributes.getNotesWithLabel('appTheme');
|
||||
|
||||
const ret = [];
|
||||
|
||||
for (const note of notes) {
|
||||
let value = await note.getLabelValue('appTheme');
|
||||
|
||||
if (!value) {
|
||||
value = note.title.toLowerCase().replace(/[^a-z0-9]/gi, '-');
|
||||
}
|
||||
|
||||
ret.push({
|
||||
val: value,
|
||||
title: note.title,
|
||||
noteId: note.noteId
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getOptions,
|
||||
updateOption,
|
||||
updateOptions
|
||||
updateOptions,
|
||||
getUserThemes
|
||||
};
|
||||
@@ -19,7 +19,7 @@ async function exec(req) {
|
||||
async function run(req) {
|
||||
const note = await repository.getNote(req.params.noteId);
|
||||
|
||||
const result = await scriptService.executeNote(note, note);
|
||||
const result = await scriptService.executeNote(note, { originEntity: note });
|
||||
|
||||
return { executionResult: result };
|
||||
}
|
||||
@@ -30,7 +30,7 @@ async function getStartupBundles() {
|
||||
const bundles = [];
|
||||
|
||||
for (const note of notes) {
|
||||
const bundle = await scriptService.getScriptBundle(note);
|
||||
const bundle = await scriptService.getScriptBundleForFrontend(note);
|
||||
|
||||
if (bundle) {
|
||||
bundles.push(bundle);
|
||||
@@ -53,14 +53,26 @@ async function getRelationBundles(req) {
|
||||
const bundles = [];
|
||||
|
||||
for (const noteId of uniqueNoteIds) {
|
||||
bundles.push(await scriptService.getScriptBundleForNoteId(noteId));
|
||||
const note = await repository.getNote(noteId);
|
||||
|
||||
if (!note.isJavaScript() || note.getScriptEnv() !== 'frontend') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bundle = await scriptService.getScriptBundleForFrontend(note);
|
||||
|
||||
if (bundle) {
|
||||
bundles.push(bundle);
|
||||
}
|
||||
}
|
||||
|
||||
return bundles;
|
||||
}
|
||||
|
||||
async function getBundle(req) {
|
||||
return await scriptService.getScriptBundleForNoteId(req.params.noteId);
|
||||
const note = await repository.getNote(req.params.noteId);
|
||||
|
||||
return await scriptService.getScriptBundleForFrontend(note);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
const repository = require('../services/repository');
|
||||
const log = require('../services/log');
|
||||
const fileUploadService = require('./api/file_upload');
|
||||
const scriptService = require('../services/script');
|
||||
|
||||
function register(router) {
|
||||
router.all('/custom/:path*', async (req, res, next) => {
|
||||
// express puts content after first slash into 0 index element
|
||||
const path = req.params.path + req.params[0];
|
||||
|
||||
const attrs = await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name IN ('customRequestHandler', 'customResourceProvider')");
|
||||
|
||||
for (const attr of attrs) {
|
||||
const regex = new RegExp(attr.value);
|
||||
let match;
|
||||
|
||||
try {
|
||||
match = path.match(regex);
|
||||
}
|
||||
catch (e) {
|
||||
log.error(`Testing path for label ${attr.attributeId}, regex=${attr.value} failed with error ` + e.stack);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attr.name === 'customRequestHandler') {
|
||||
const note = await attr.getNote();
|
||||
|
||||
log.info(`Handling custom request "${path}" with note ${note.noteId}`);
|
||||
|
||||
try {
|
||||
await scriptService.executeNote(note, {
|
||||
pathParams: match.slice(1),
|
||||
req,
|
||||
res
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
log.error(`Custom handler ${note.noteId} failed with ${e.message}`);
|
||||
|
||||
res.status(500).send(e.message);
|
||||
}
|
||||
}
|
||||
else if (attr.name === 'customResourceProvider') {
|
||||
await fileUploadService.downloadNoteFile(attr.noteId, res);
|
||||
}
|
||||
else {
|
||||
throw new Error("Unrecognized attribute name " + attr.name);
|
||||
}
|
||||
|
||||
return; // only first handler is executed
|
||||
}
|
||||
|
||||
const message = `No handler matched for custom ${path} request.`;
|
||||
|
||||
log.info(message);
|
||||
res.status(404).send(message);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
register
|
||||
};
|
||||
+7
-13
@@ -16,25 +16,19 @@ async function index(req, res) {
|
||||
leftPaneMinWidth: parseInt(options.leftPaneMinWidth),
|
||||
leftPaneWidthPercent: parseInt(options.leftPaneWidthPercent),
|
||||
rightPaneWidthPercent: 100 - parseInt(options.leftPaneWidthPercent),
|
||||
mainFontSize: parseInt(options.mainFontSize),
|
||||
treeFontSize: parseInt(options.treeFontSize),
|
||||
detailFontSize: parseInt(options.detailFontSize),
|
||||
sourceId: await sourceIdService.generateSourceId(),
|
||||
maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync"),
|
||||
instanceName: config.General ? config.General.instanceName : null,
|
||||
appCss: await getAppCss()
|
||||
appCssNoteIds: await getAppCssNoteIds()
|
||||
});
|
||||
}
|
||||
|
||||
async function getAppCss() {
|
||||
let css = '';
|
||||
const notes = attributeService.getNotesWithLabel('appCss');
|
||||
|
||||
for (const note of await notes) {
|
||||
css += `/* ${note.noteId} */
|
||||
${note.content}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
return css;
|
||||
async function getAppCssNoteIds() {
|
||||
return (await attributeService.getNotesWithLabels(['appCss', 'appTheme']))
|
||||
.map(note => note.noteId);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -135,6 +135,8 @@ function register(app) {
|
||||
filesRoute.uploadFile, apiResultHandler);
|
||||
|
||||
route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile);
|
||||
// this "hacky" path is used for easier referencing of CSS resources
|
||||
route(GET, '/api/notes/download/:noteId', [auth.checkApiAuthOrElectron], filesRoute.downloadFile);
|
||||
|
||||
apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getEffectiveNoteAttributes);
|
||||
apiRoute(PUT, '/api/notes/:noteId/attributes', attributesRoute.updateNoteAttributes);
|
||||
@@ -153,6 +155,7 @@ function register(app) {
|
||||
apiRoute(GET, '/api/options', optionsApiRoute.getOptions);
|
||||
apiRoute(PUT, '/api/options/:name/:value*', optionsApiRoute.updateOption);
|
||||
apiRoute(PUT, '/api/options', optionsApiRoute.updateOptions);
|
||||
apiRoute(GET, '/api/options/user-themes', optionsApiRoute.getUserThemes);
|
||||
|
||||
apiRoute(POST, '/api/password/change', passwordApiRoute.changePassword);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const path = require('path');
|
||||
const {APP_PNG_ICON_DIR, ELECTRON_APP_ROOT_DIR} = require("./resource_dir");
|
||||
const {ELECTRON_APP_ROOT_DIR} = require("./resource_dir");
|
||||
const log = require("./log");
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
@@ -11,7 +11,7 @@ const utils = require('./utils');
|
||||
const template = `[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Trilium Notes
|
||||
Icon=#APP_PNG_ICON_DIR#/128x128.png
|
||||
Icon=#APP_ROOT_DIR#/icon.png
|
||||
Exec=#EXE_PATH#
|
||||
Categories=Office
|
||||
Terminal=false
|
||||
@@ -50,7 +50,7 @@ function installLocalAppIcon() {
|
||||
|
||||
function getDesktopFileContent() {
|
||||
return template
|
||||
.replace("#APP_PNG_ICON_DIR#", escapePath(APP_PNG_ICON_DIR))
|
||||
.replace("#APP_ROOT_DIR#", escapePath(ELECTRON_APP_ROOT_DIR))
|
||||
.replace("#EXE_PATH#", escapePath(getExePath()));
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ const build = require('./build');
|
||||
const packageJson = require('../../package');
|
||||
const {TRILIUM_DATA_DIR} = require('./data_dir');
|
||||
|
||||
const APP_DB_VERSION = 121;
|
||||
const SYNC_VERSION = 3;
|
||||
const APP_DB_VERSION = 124;
|
||||
const SYNC_VERSION = 4;
|
||||
|
||||
module.exports = {
|
||||
appVersion: packageJson.version,
|
||||
|
||||
@@ -15,8 +15,12 @@ const BUILTIN_ATTRIBUTES = [
|
||||
{ type: 'label', name: 'manualTransactionHandling' },
|
||||
{ type: 'label', name: 'disableInclusion' },
|
||||
{ type: 'label', name: 'appCss' },
|
||||
{ type: 'label', name: 'appTheme' },
|
||||
{ type: 'label', name: 'hideChildrenOverview' },
|
||||
{ type: 'label', name: 'hidePromotedAttributes' },
|
||||
{ type: 'label', name: 'readOnly' },
|
||||
{ type: 'label', name: 'customRequestHandler' },
|
||||
{ type: 'label', name: 'customResourceProvider' },
|
||||
|
||||
// relation names
|
||||
{ type: 'relation', name: 'runOnNoteView' },
|
||||
@@ -43,6 +47,13 @@ async function getNotesWithLabel(name, value) {
|
||||
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? ${valueCondition} ORDER BY position`, params);
|
||||
}
|
||||
|
||||
async function getNotesWithLabels(names) {
|
||||
const questionMarks = names.map(() => "?").join(", ");
|
||||
|
||||
return await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)
|
||||
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name IN (${questionMarks}) ORDER BY position`, names);
|
||||
}
|
||||
|
||||
async function getNoteWithLabel(name, value) {
|
||||
const notes = await getNotesWithLabel(name, value);
|
||||
|
||||
@@ -85,6 +96,7 @@ async function getAttributeNames(type, nameLike) {
|
||||
|
||||
module.exports = {
|
||||
getNotesWithLabel,
|
||||
getNotesWithLabels,
|
||||
getNoteWithLabel,
|
||||
createLabel,
|
||||
createAttribute,
|
||||
|
||||
@@ -11,6 +11,7 @@ const repository = require('./repository');
|
||||
const axios = require('axios');
|
||||
const cloningService = require('./cloning');
|
||||
const messagingService = require('./messaging');
|
||||
const appInfo = require('./app_info');
|
||||
|
||||
/**
|
||||
* This is the main backend API interface for scripts. It's published in the local "api" object.
|
||||
@@ -18,13 +19,17 @@ const messagingService = require('./messaging');
|
||||
* @constructor
|
||||
* @hideconstructor
|
||||
*/
|
||||
function BackendScriptApi(startNote, currentNote, originEntity) {
|
||||
function BackendScriptApi(currentNote, apiParams) {
|
||||
/** @property {Note} note where script started executing */
|
||||
this.startNote = startNote;
|
||||
this.startNote = apiParams.startNote;
|
||||
/** @property {Note} note where script is currently executing */
|
||||
this.currentNote = currentNote;
|
||||
/** @property {Entity} entity whose event triggered this executions */
|
||||
this.originEntity = originEntity;
|
||||
this.originEntity = apiParams.originEntity;
|
||||
|
||||
for (const key in apiParams) {
|
||||
this[key] = apiParams[key];
|
||||
}
|
||||
|
||||
this.axios = axios;
|
||||
|
||||
@@ -168,6 +173,23 @@ function BackendScriptApi(startNote, currentNote, originEntity) {
|
||||
*/
|
||||
this.createNote = noteService.createNote;
|
||||
|
||||
/**
|
||||
* Creates new note according to given params and force all connected clients to refresh their tree.
|
||||
*
|
||||
* @method
|
||||
*
|
||||
* @param {string} parentNoteId - create new note under this parent
|
||||
* @param {string} title
|
||||
* @param {string} [content=""]
|
||||
* @param {CreateNoteExtraOptions} [extraOptions={}]
|
||||
* @returns {Promise<{note: Note, branch: Branch}>} object contains newly created entities note and branch
|
||||
*/
|
||||
this.createNoteAndRefresh = async function(parentNoteId, title, content, extraOptions) {
|
||||
await noteService.createNote(parentNoteId, title, content, extraOptions);
|
||||
|
||||
messagingService.refreshTree();
|
||||
};
|
||||
|
||||
/**
|
||||
* Log given message to trilium logs.
|
||||
*
|
||||
@@ -233,7 +255,12 @@ function BackendScriptApi(startNote, currentNote, originEntity) {
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
this.refreshTree = () => messagingService.sendMessageToAllClients({ type: 'refresh-tree' });
|
||||
this.refreshTree = messagingService.refreshTree;
|
||||
|
||||
/**
|
||||
* @return {{syncVersion, appVersion, buildRevision, dbVersion, dataDirectory, buildDate}|*} - object representing basic info about running Trilium version
|
||||
*/
|
||||
this.getAppInfo = () => appInfo
|
||||
}
|
||||
|
||||
module.exports = BackendScriptApi;
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2019-01-10T21:31:30+01:00", buildRevision: "0b251530fa0ee61edc8dcc9235033abb73afc614" };
|
||||
module.exports = { buildDate:"2019-02-12T20:30:07+01:00", buildRevision: "8b250ed523f851afc0923342827679e805329838" };
|
||||
|
||||
@@ -8,6 +8,10 @@ const repository = require('./repository');
|
||||
const Branch = require('../entities/branch');
|
||||
|
||||
async function cloneNoteToParent(noteId, parentNoteId, prefix) {
|
||||
if (await isNoteDeleted(noteId) || await isNoteDeleted(parentNoteId)) {
|
||||
return { success: false, message: 'Note is deleted.' };
|
||||
}
|
||||
|
||||
const validationResult = await treeService.validateParentChild(parentNoteId, noteId);
|
||||
|
||||
if (!validationResult.success) {
|
||||
@@ -27,6 +31,10 @@ async function cloneNoteToParent(noteId, parentNoteId, prefix) {
|
||||
}
|
||||
|
||||
async function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) {
|
||||
if (await isNoteDeleted(noteId) || await isNoteDeleted(parentNoteId)) {
|
||||
return { success: false, message: 'Note is deleted.' };
|
||||
}
|
||||
|
||||
const validationResult = await treeService.validateParentChild(parentNoteId, noteId);
|
||||
|
||||
if (!validationResult.success) {
|
||||
@@ -61,6 +69,10 @@ async function toggleNoteInParent(present, noteId, parentNoteId, prefix) {
|
||||
async function cloneNoteAfter(noteId, afterBranchId) {
|
||||
const afterNote = await treeService.getBranch(afterBranchId);
|
||||
|
||||
if (await isNoteDeleted(noteId) || await isNoteDeleted(afterNote.parentNoteId)) {
|
||||
return { success: false, message: 'Note is deleted.' };
|
||||
}
|
||||
|
||||
const validationResult = await treeService.validateParentChild(afterNote.parentNoteId, noteId);
|
||||
|
||||
if (!validationResult.result) {
|
||||
@@ -84,6 +96,12 @@ async function cloneNoteAfter(noteId, afterBranchId) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async function isNoteDeleted(noteId) {
|
||||
const note = await repository.getNote(noteId);
|
||||
|
||||
return note.isDeleted;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
cloneNoteToParent,
|
||||
ensureNoteIsPresentInParent,
|
||||
|
||||
+341
-218
@@ -5,23 +5,39 @@ const sqlInit = require('./sql_init');
|
||||
const log = require('./log');
|
||||
const messagingService = require('./messaging');
|
||||
const syncMutexService = require('./sync_mutex');
|
||||
const repository = require('./repository.js');
|
||||
const repository = require('./repository');
|
||||
const cls = require('./cls');
|
||||
const syncTableService = require('./sync_table');
|
||||
const Branch = require('../entities/branch');
|
||||
|
||||
async function runCheck(query, errorText, errorList) {
|
||||
const result = await sql.getColumn(query);
|
||||
let unrecoverableConsistencyErrors = false;
|
||||
let fixedIssues = false;
|
||||
|
||||
if (result.length > 0) {
|
||||
const resultText = result.map(val => "'" + val + "'").join(', ');
|
||||
async function findIssues(query, errorCb) {
|
||||
const results = await sql.getRows(query);
|
||||
|
||||
const err = errorText + ": " + resultText;
|
||||
errorList.push(err);
|
||||
for (const res of results) {
|
||||
logError(errorCb(res));
|
||||
|
||||
log.error(err);
|
||||
unrecoverableConsistencyErrors = true;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function checkTreeCycles(errorList) {
|
||||
async function findAndFixIssues(query, fixerCb) {
|
||||
const results = await sql.getRows(query);
|
||||
|
||||
for (const res of results) {
|
||||
await fixerCb(res);
|
||||
|
||||
fixedIssues = true;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function checkTreeCycles() {
|
||||
const childToParents = {};
|
||||
const rows = await sql.getRows("SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0");
|
||||
|
||||
@@ -33,25 +49,29 @@ async function checkTreeCycles(errorList) {
|
||||
childToParents[childNoteId].push(parentNoteId);
|
||||
}
|
||||
|
||||
function checkTreeCycle(noteId, path, errorList) {
|
||||
function checkTreeCycle(noteId, path) {
|
||||
if (noteId === 'root') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!childToParents[noteId] || childToParents[noteId].length === 0) {
|
||||
errorList.push(`No parents found for noteId=${noteId}`);
|
||||
logError(`No parents found for note ${noteId}`);
|
||||
|
||||
unrecoverableConsistencyErrors = true;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const parentNoteId of childToParents[noteId]) {
|
||||
if (path.includes(parentNoteId)) {
|
||||
errorList.push(`Tree cycle detected at parent-child relationship: ${parentNoteId} - ${noteId}, whole path: ${path}`);
|
||||
logError(`Tree cycle detected at parent-child relationship: ${parentNoteId} - ${noteId}, whole path: ${path}`);
|
||||
|
||||
unrecoverableConsistencyErrors = true;
|
||||
}
|
||||
else {
|
||||
const newPath = path.slice();
|
||||
newPath.push(noteId);
|
||||
|
||||
checkTreeCycle(parentNoteId, newPath, errorList);
|
||||
checkTreeCycle(parentNoteId, newPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,268 +79,365 @@ async function checkTreeCycles(errorList) {
|
||||
const noteIds = Object.keys(childToParents);
|
||||
|
||||
for (const noteId of noteIds) {
|
||||
checkTreeCycle(noteId, [], errorList);
|
||||
checkTreeCycle(noteId, []);
|
||||
}
|
||||
|
||||
if (childToParents['root'].length !== 1 || childToParents['root'][0] !== 'none') {
|
||||
errorList.push('Incorrect root parent: ' + JSON.stringify(childToParents['root']));
|
||||
logError('Incorrect root parent: ' + JSON.stringify(childToParents['root']));
|
||||
unrecoverableConsistencyErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function runSyncRowChecks(table, key, errorList) {
|
||||
await runCheck(`
|
||||
SELECT
|
||||
${key}
|
||||
FROM
|
||||
${table}
|
||||
LEFT JOIN sync ON sync.entityName = '${table}' AND entityId = ${key}
|
||||
WHERE
|
||||
sync.id IS NULL AND ` + (table === 'options' ? 'isSynced = 1' : '1'),
|
||||
`Missing sync records for ${key} in table ${table}`, errorList);
|
||||
async function findBrokenReferenceIssues() {
|
||||
await findIssues(`
|
||||
SELECT branchId, branches.noteId
|
||||
FROM branches LEFT JOIN notes USING(noteId)
|
||||
WHERE branches.isDeleted = 0 AND notes.noteId IS NULL`,
|
||||
({branchId, noteId}) => `Branch ${branchId} references missing note ${noteId}`);
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
entityId
|
||||
FROM
|
||||
sync
|
||||
LEFT JOIN ${table} ON entityId = ${key}
|
||||
WHERE
|
||||
sync.entityName = '${table}'
|
||||
AND ${key} IS NULL`,
|
||||
`Missing ${table} records for existing sync rows`, errorList);
|
||||
await findIssues(`
|
||||
SELECT branchId, branches.noteId AS parentNoteId
|
||||
FROM branches LEFT JOIN notes ON notes.noteId = branches.parentNoteId
|
||||
WHERE branches.isDeleted = 0 AND branches.branchId != 'root' AND notes.noteId IS NULL`,
|
||||
({branchId, noteId}) => `Branch ${branchId} references missing parent note ${noteId}`);
|
||||
|
||||
await findIssues(`
|
||||
SELECT attributeId, attributes.noteId
|
||||
FROM attributes LEFT JOIN notes USING(noteId)
|
||||
WHERE attributes.isDeleted = 0 AND notes.noteId IS NULL`,
|
||||
({attributeId, noteId}) => `Attribute ${attributeId} references missing source note ${noteId}`);
|
||||
|
||||
// empty targetNoteId for relations is a special fixable case so not covered here
|
||||
await findIssues(`
|
||||
SELECT attributeId, attributes.value AS noteId
|
||||
FROM attributes LEFT JOIN notes ON notes.noteId = attributes.value
|
||||
WHERE attributes.isDeleted = 0 AND attributes.type = 'relation'
|
||||
AND attributes.value != '' AND notes.noteId IS NULL`,
|
||||
({attributeId, noteId}) => `Relation ${attributeId} references missing note ${noteId}`);
|
||||
|
||||
await findIssues(`
|
||||
SELECT linkId, links.noteId
|
||||
FROM links LEFT JOIN notes USING(noteId)
|
||||
WHERE links.isDeleted = 0 AND notes.noteId IS NULL`,
|
||||
({linkId, noteId}) => `Link ${linkId} references missing source note ${noteId}`);
|
||||
|
||||
await findIssues(`
|
||||
SELECT linkId, links.noteId
|
||||
FROM links LEFT JOIN notes ON notes.noteId = links.targetNoteId
|
||||
WHERE links.isDeleted = 0 AND notes.noteId IS NULL`,
|
||||
({linkId, noteId}) => `Link ${linkId} references missing target note ${noteId}`);
|
||||
|
||||
await findIssues(`
|
||||
SELECT noteRevisionId, note_revisions.noteId
|
||||
FROM note_revisions LEFT JOIN notes USING(noteId)
|
||||
WHERE notes.noteId IS NULL`,
|
||||
({noteRevisionId, noteId}) => `Note revision ${noteRevisionId} references missing note ${noteId}`);
|
||||
}
|
||||
|
||||
async function fixEmptyRelationTargets(errorList) {
|
||||
const emptyRelations = await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'relation' AND value = ''");
|
||||
async function findExistencyIssues() {
|
||||
// principle for fixing inconsistencies is that if the note itself is deleted (isDeleted=true) then all related entities should be also deleted (branches, links, attributes)
|
||||
// but if note is not deleted, then at least one branch should exist.
|
||||
|
||||
for (const relation of emptyRelations) {
|
||||
relation.isDeleted = true;
|
||||
await relation.save();
|
||||
|
||||
errorList.push(`Relation ${relation.attributeId} of name "${relation.name} has empty target. Autofixed.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function runAllChecks() {
|
||||
const errorList = [];
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
noteId
|
||||
FROM
|
||||
notes
|
||||
LEFT JOIN branches USING(noteId)
|
||||
WHERE
|
||||
noteId != 'root'
|
||||
AND branches.branchId IS NULL`,
|
||||
"Missing branches records for following note IDs", errorList);
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
branchId || ' > ' || branches.noteId
|
||||
FROM
|
||||
branches
|
||||
LEFT JOIN notes USING(noteId)
|
||||
WHERE
|
||||
notes.noteId IS NULL`,
|
||||
"Missing notes records for following branch ID > note ID", errorList);
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
branchId
|
||||
FROM
|
||||
branches
|
||||
JOIN notes USING(noteId)
|
||||
WHERE
|
||||
notes.isDeleted = 1
|
||||
AND branches.isDeleted = 0`,
|
||||
"Branch is not deleted even though main note is deleted for following branch IDs", errorList);
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
child.branchId
|
||||
FROM
|
||||
branches AS child
|
||||
WHERE
|
||||
child.isDeleted = 0
|
||||
AND child.parentNoteId != 'none'
|
||||
AND (SELECT COUNT(*) FROM branches AS parent WHERE parent.noteId = child.parentNoteId
|
||||
AND parent.isDeleted = 0) = 0`,
|
||||
"All parent branches are deleted but child branch is not for these child branch IDs", errorList);
|
||||
|
||||
// we do extra JOIN to eliminate orphan notes without branches (which are reported separately)
|
||||
await runCheck(`
|
||||
// the order here is important - first we might need to delete inconsistent branches and after that
|
||||
// another check might create missing branch
|
||||
await findAndFixIssues(`
|
||||
SELECT
|
||||
DISTINCT noteId
|
||||
branchId, noteId
|
||||
FROM
|
||||
notes
|
||||
JOIN branches USING(noteId)
|
||||
branches
|
||||
JOIN notes USING(noteId)
|
||||
WHERE
|
||||
(SELECT COUNT(*) FROM branches WHERE notes.noteId = branches.noteId AND branches.isDeleted = 0) = 0
|
||||
AND notes.isDeleted = 0
|
||||
`, 'No undeleted branches for note IDs', errorList);
|
||||
notes.isDeleted = 1
|
||||
AND branches.isDeleted = 0`,
|
||||
async ({branchId, noteId}) => {
|
||||
const branch = await repository.getBranch(branchId);
|
||||
branch.isDeleted = true;
|
||||
await branch.save();
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
child.parentNoteId || ' > ' || child.noteId
|
||||
FROM branches
|
||||
AS child
|
||||
LEFT JOIN branches AS parent ON parent.noteId = child.parentNoteId
|
||||
WHERE
|
||||
parent.noteId IS NULL
|
||||
AND child.parentNoteId != 'none'`,
|
||||
"Not existing parent in the following parent > child relations", errorList);
|
||||
logFix(`Branch ${branchId} has been deleted since associated note ${noteId} is deleted.`);
|
||||
});
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
noteRevisionId || ' > ' || note_revisions.noteId
|
||||
FROM
|
||||
note_revisions LEFT JOIN notes USING(noteId)
|
||||
WHERE
|
||||
notes.noteId IS NULL`,
|
||||
"Missing notes records for following note revision ID > note ID", errorList);
|
||||
await findAndFixIssues(`
|
||||
SELECT
|
||||
branchId, parentNoteId
|
||||
FROM
|
||||
branches
|
||||
JOIN notes AS parentNote ON parentNote.noteId = branches.parentNoteId
|
||||
WHERE
|
||||
parentNote.isDeleted = 1
|
||||
AND branches.isDeleted = 0
|
||||
`, async ({branchId, parentNoteId}) => {
|
||||
const branch = await repository.getBranch(branchId);
|
||||
branch.isDeleted = true;
|
||||
await branch.save();
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
branches.parentNoteId || ' > ' || branches.noteId
|
||||
FROM
|
||||
branches
|
||||
WHERE
|
||||
branches.isDeleted = 0
|
||||
GROUP BY
|
||||
branches.parentNoteId,
|
||||
branches.noteId
|
||||
HAVING
|
||||
COUNT(*) > 1`,
|
||||
"Duplicate undeleted parent note <-> note relationship - parent note ID > note ID", errorList);
|
||||
logFix(`Branch ${branchId} has been deleted since associated parent note ${parentNoteId} is deleted.`);
|
||||
});
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
noteId
|
||||
FROM
|
||||
notes
|
||||
WHERE
|
||||
type != 'text'
|
||||
AND type != 'code'
|
||||
AND type != 'render'
|
||||
AND type != 'file'
|
||||
AND type != 'image'
|
||||
AND type != 'search'
|
||||
AND type != 'relation-map'`,
|
||||
"Note has invalid type", errorList);
|
||||
await findAndFixIssues(`
|
||||
SELECT
|
||||
DISTINCT notes.noteId
|
||||
FROM
|
||||
notes
|
||||
LEFT JOIN branches ON notes.noteId = branches.noteId AND branches.isDeleted = 0
|
||||
WHERE
|
||||
notes.isDeleted = 0
|
||||
AND branches.branchId IS NULL
|
||||
`, async ({noteId}) => {
|
||||
const branch = await new Branch({
|
||||
parentNoteId: 'root',
|
||||
noteId: noteId,
|
||||
prefix: 'recovered'
|
||||
}).save();
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
noteId
|
||||
FROM
|
||||
notes
|
||||
logFix(`Created missing branch ${branch.branchId} for note ${noteId}`);
|
||||
});
|
||||
|
||||
// there should be a unique relationship between note and its parent
|
||||
await findAndFixIssues(`
|
||||
SELECT
|
||||
noteId, parentNoteId
|
||||
FROM
|
||||
branches
|
||||
WHERE
|
||||
branches.isDeleted = 0
|
||||
GROUP BY
|
||||
branches.parentNoteId,
|
||||
branches.noteId
|
||||
HAVING
|
||||
COUNT(*) > 1`,
|
||||
async ({noteId, parentNoteId}) => {
|
||||
const branches = await repository.getEntities(`SELECT * FROM branches WHERE noteId = ? and parentNoteId = ? and isDeleted = 1`, [noteId, parentNoteId]);
|
||||
|
||||
// it's not necessarily "original" branch, it's just the only one which will survive
|
||||
const origBranch = branches.get(0);
|
||||
|
||||
// delete all but the first branch
|
||||
for (const branch of branches.slice(1)) {
|
||||
branch.isDeleted = true;
|
||||
await branch.save();
|
||||
|
||||
logFix(`Removing branch ${branch.branchId} since it's parent-child duplicate of branch ${origBranch.branchId}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function findLogicIssues() {
|
||||
await findIssues( `
|
||||
SELECT noteId, type
|
||||
FROM notes
|
||||
WHERE
|
||||
isDeleted = 0
|
||||
AND type NOT IN ('text', 'code', 'render', 'file', 'image', 'search', 'relation-map')`,
|
||||
({noteId, type}) => `Note ${noteId} has invalid type=${type}`);
|
||||
|
||||
await findIssues(`
|
||||
SELECT noteId
|
||||
FROM notes
|
||||
WHERE
|
||||
isDeleted = 0
|
||||
AND content IS NULL`,
|
||||
"Note content is null even though it is not deleted", errorList);
|
||||
({noteId}) => `Note ${noteId} content is null even though it is not deleted`);
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
parentNoteId
|
||||
await findIssues(`
|
||||
SELECT parentNoteId
|
||||
FROM
|
||||
branches
|
||||
JOIN notes ON notes.noteId = branches.parentNoteId
|
||||
WHERE
|
||||
notes.isDeleted = 0
|
||||
AND notes.type == 'search'
|
||||
AND branches.isDeleted = 0`,
|
||||
({parentNoteId}) => `Search note ${parentNoteId} has children`);
|
||||
|
||||
await findAndFixIssues(`
|
||||
SELECT attributeId
|
||||
FROM attributes
|
||||
WHERE
|
||||
type == 'search'`,
|
||||
"Search note has children", errorList);
|
||||
isDeleted = 0
|
||||
AND type = 'relation'
|
||||
AND value = ''`,
|
||||
async ({attributeId}) => {
|
||||
const relation = await repository.getAttribute(attributeId);
|
||||
relation.isDeleted = true;
|
||||
await relation.save();
|
||||
|
||||
await fixEmptyRelationTargets(errorList);
|
||||
logFix(`Removed relation ${relation.attributeId} of name "${relation.name} with empty target.`);
|
||||
});
|
||||
|
||||
await runCheck(`
|
||||
await findIssues(`
|
||||
SELECT
|
||||
attributeId
|
||||
FROM
|
||||
attributes
|
||||
attributeId,
|
||||
type
|
||||
FROM attributes
|
||||
WHERE
|
||||
type != 'label'
|
||||
isDeleted = 0
|
||||
AND type != 'label'
|
||||
AND type != 'label-definition'
|
||||
AND type != 'relation'
|
||||
AND type != 'relation-definition'`,
|
||||
"Attribute has invalid type", errorList);
|
||||
({attributeId, type}) => `Attribute ${attributeId} has invalid type '${type}'`);
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
attributeId
|
||||
FROM
|
||||
attributes
|
||||
LEFT JOIN notes ON attributes.noteId = notes.noteId AND notes.isDeleted = 0
|
||||
WHERE
|
||||
attributes.isDeleted = 0
|
||||
AND notes.noteId IS NULL`,
|
||||
"Attribute reference to the owning note is broken", errorList);
|
||||
|
||||
await runCheck(`
|
||||
await findAndFixIssues(`
|
||||
SELECT
|
||||
attributeId
|
||||
attributeId,
|
||||
attributes.noteId
|
||||
FROM
|
||||
attributes
|
||||
LEFT JOIN notes AS targetNote ON attributes.value = targetNote.noteId AND targetNote.isDeleted = 0
|
||||
JOIN notes ON attributes.noteId = notes.noteId
|
||||
WHERE
|
||||
attributes.isDeleted = 0
|
||||
AND notes.isDeleted = 1`,
|
||||
async ({attributeId, noteId}) => {
|
||||
const attribute = await repository.getAttribute(attributeId);
|
||||
attribute.isDeleted = true;
|
||||
await attribute.save();
|
||||
|
||||
logFix(`Removed attribute ${attributeId} because owning note ${noteId} is also deleted.`);
|
||||
});
|
||||
|
||||
await findAndFixIssues(`
|
||||
SELECT
|
||||
attributeId,
|
||||
attributes.value AS targetNoteId
|
||||
FROM
|
||||
attributes
|
||||
JOIN notes ON attributes.value = notes.noteId
|
||||
WHERE
|
||||
attributes.type = 'relation'
|
||||
AND attributes.isDeleted = 0
|
||||
AND targetNote.noteId IS NULL`,
|
||||
"Relation reference to the target note is broken", errorList);
|
||||
AND notes.isDeleted = 1`,
|
||||
async ({attributeId, targetNoteId}) => {
|
||||
const attribute = await repository.getAttribute(attributeId);
|
||||
attribute.isDeleted = true;
|
||||
await attribute.save();
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
linkId
|
||||
FROM
|
||||
links
|
||||
WHERE
|
||||
type != 'image'
|
||||
AND type != 'hyper'
|
||||
AND type != 'relation-map'`,
|
||||
"Link type is invalid", errorList);
|
||||
logFix(`Removed attribute ${attributeId} because target note ${targetNoteId} is also deleted.`);
|
||||
});
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
linkId
|
||||
FROM
|
||||
await findIssues(`
|
||||
SELECT linkId
|
||||
FROM links
|
||||
WHERE type NOT IN ('image', 'hyper', 'relation-map')`,
|
||||
({linkId, type}) => `Link ${linkId} has invalid type '${type}'`);
|
||||
|
||||
await findAndFixIssues(`
|
||||
SELECT
|
||||
linkId,
|
||||
links.noteId AS sourceNoteId
|
||||
FROM
|
||||
links
|
||||
LEFT JOIN notes AS sourceNote ON sourceNote.noteId = links.noteId AND sourceNote.isDeleted = 0
|
||||
LEFT JOIN notes AS targetNote ON targetNote.noteId = links.noteId AND targetNote.isDeleted = 0
|
||||
WHERE
|
||||
JOIN notes AS sourceNote ON sourceNote.noteId = links.noteId
|
||||
WHERE
|
||||
links.isDeleted = 0
|
||||
AND (sourceNote.noteId IS NULL
|
||||
OR targetNote.noteId IS NULL)`,
|
||||
"Link to source/target note link is broken", errorList);
|
||||
AND sourceNote.isDeleted = 1`,
|
||||
async ({linkId, sourceNoteId}) => {
|
||||
const link = await repository.getLink(linkId);
|
||||
link.isDeleted = true;
|
||||
await link.save();
|
||||
|
||||
await runSyncRowChecks("notes", "noteId", errorList);
|
||||
await runSyncRowChecks("note_revisions", "noteRevisionId", errorList);
|
||||
await runSyncRowChecks("branches", "branchId", errorList);
|
||||
await runSyncRowChecks("recent_notes", "branchId", errorList);
|
||||
await runSyncRowChecks("attributes", "attributeId", errorList);
|
||||
await runSyncRowChecks("api_tokens", "apiTokenId", errorList);
|
||||
await runSyncRowChecks("options", "name", errorList);
|
||||
logFix(`Removed link ${linkId} because source note ${sourceNoteId} is also deleted.`);
|
||||
});
|
||||
|
||||
if (errorList.length === 0) {
|
||||
await findAndFixIssues(`
|
||||
SELECT
|
||||
linkId,
|
||||
links.targetNoteId
|
||||
FROM
|
||||
links
|
||||
JOIN notes AS targetNote ON targetNote.noteId = links.targetNoteId
|
||||
WHERE
|
||||
links.isDeleted = 0
|
||||
AND targetNote.isDeleted = 1`,
|
||||
async ({linkId, targetNoteId}) => {
|
||||
const link = await repository.getLink(linkId);
|
||||
link.isDeleted = true;
|
||||
await link.save();
|
||||
|
||||
logFix(`Removed link ${linkId} because target note ${targetNoteId} is also deleted.`);
|
||||
});
|
||||
}
|
||||
|
||||
async function runSyncRowChecks(entityName, key) {
|
||||
await findAndFixIssues(`
|
||||
SELECT
|
||||
${key} as entityId
|
||||
FROM
|
||||
${entityName}
|
||||
LEFT JOIN sync ON sync.entityName = '${entityName}' AND entityId = ${key}
|
||||
WHERE
|
||||
sync.id IS NULL AND ` + (entityName === 'options' ? 'isSynced = 1' : '1'),
|
||||
async ({entityId}) => {
|
||||
await syncTableService.addEntitySync(entityName, entityId);
|
||||
|
||||
logFix(`Created missing sync record entityName=${entityName}, entityId=${entityId}`);
|
||||
});
|
||||
|
||||
await findAndFixIssues(`
|
||||
SELECT
|
||||
id, entityId
|
||||
FROM
|
||||
sync
|
||||
LEFT JOIN ${entityName} ON entityId = ${key}
|
||||
WHERE
|
||||
sync.entityName = '${entityName}'
|
||||
AND ${key} IS NULL`,
|
||||
async ({id, entityId}) => {
|
||||
|
||||
await sql.execute("DELETE FROM sync WHERE entityName = ? AND entityId = ?", [entityName, entityId]);
|
||||
|
||||
logFix(`Deleted extra sync record id=${id}, entityName=${entityName}, entityId=${entityId}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function findSyncRowsIssues() {
|
||||
await runSyncRowChecks("notes", "noteId");
|
||||
await runSyncRowChecks("note_revisions", "noteRevisionId");
|
||||
await runSyncRowChecks("branches", "branchId");
|
||||
await runSyncRowChecks("recent_notes", "branchId");
|
||||
await runSyncRowChecks("attributes", "attributeId");
|
||||
await runSyncRowChecks("api_tokens", "apiTokenId");
|
||||
await runSyncRowChecks("options", "name");
|
||||
}
|
||||
|
||||
async function runAllChecks() {
|
||||
unrecoverableConsistencyErrors = false;
|
||||
fixedIssues = false;
|
||||
|
||||
await findBrokenReferenceIssues();
|
||||
|
||||
await findExistencyIssues();
|
||||
|
||||
await findLogicIssues();
|
||||
|
||||
await findSyncRowsIssues();
|
||||
|
||||
if (unrecoverableConsistencyErrors) {
|
||||
// we run this only if basic checks passed since this assumes basic data consistency
|
||||
|
||||
await checkTreeCycles(errorList);
|
||||
await checkTreeCycles();
|
||||
}
|
||||
|
||||
return errorList;
|
||||
return !unrecoverableConsistencyErrors;
|
||||
}
|
||||
|
||||
async function runChecks() {
|
||||
let errorList;
|
||||
let elapsedTimeMs;
|
||||
|
||||
await syncMutexService.doExclusively(async () => {
|
||||
const startTime = new Date();
|
||||
|
||||
errorList = await runAllChecks();
|
||||
await runAllChecks();
|
||||
|
||||
elapsedTimeMs = new Date().getTime() - startTime.getTime();
|
||||
});
|
||||
|
||||
if (errorList.length > 0) {
|
||||
log.info(`Consistency checks failed (took ${elapsedTimeMs}ms) with these errors: ` + JSON.stringify(errorList));
|
||||
if (fixedIssues) {
|
||||
messagingService.refreshTree();
|
||||
}
|
||||
|
||||
if (unrecoverableConsistencyErrors) {
|
||||
log.info(`Consistency checks failed (took ${elapsedTimeMs}ms)`);
|
||||
|
||||
messagingService.sendMessageToAllClients({type: 'consistency-checks-failed'});
|
||||
}
|
||||
@@ -329,13 +446,19 @@ async function runChecks() {
|
||||
}
|
||||
}
|
||||
|
||||
function logFix(message) {
|
||||
log.info("Consistency issue fixed: " + message);
|
||||
}
|
||||
|
||||
function logError(message) {
|
||||
log.info("Consistency error: " + message);
|
||||
}
|
||||
|
||||
sqlInit.dbReady.then(() => {
|
||||
setInterval(cls.wrap(runChecks), 60 * 60 * 1000);
|
||||
|
||||
// kickoff backup immediately
|
||||
setTimeout(cls.wrap(runChecks), 10000);
|
||||
// kickoff checks soon after startup (to not block the initial load)
|
||||
setTimeout(cls.wrap(runChecks), 10 * 1000);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
runChecks
|
||||
};
|
||||
module.exports = {};
|
||||
@@ -18,25 +18,26 @@ function shaArray(content) {
|
||||
}
|
||||
|
||||
function pad(data) {
|
||||
let padded = Array.from(data);
|
||||
|
||||
if (data.length >= 16) {
|
||||
padded = padded.slice(0, 16);
|
||||
if (data.length > 16) {
|
||||
data = data.slice(0, 16);
|
||||
}
|
||||
else {
|
||||
padded = padded.concat(Array(16 - padded.length).fill(0));
|
||||
else if (data.length < 16) {
|
||||
const zeros = Array(16 - data.length).fill(0);
|
||||
|
||||
data = Buffer.concat([data, Buffer.from(zeros)]);
|
||||
}
|
||||
|
||||
return Buffer.from(padded);
|
||||
return Buffer.from(data);
|
||||
}
|
||||
|
||||
function encrypt(key, iv, plainText) {
|
||||
function encrypt(key, plainText, ivLength = 13) {
|
||||
if (!key) {
|
||||
throw new Error("No data key!");
|
||||
}
|
||||
|
||||
const plainTextBuffer = Buffer.from(plainText);
|
||||
|
||||
const iv = crypto.randomBytes(ivLength);
|
||||
const cipher = crypto.createCipheriv('aes-128-cbc', pad(key), pad(iv));
|
||||
|
||||
const digest = shaArray(plainTextBuffer).slice(0, 4);
|
||||
@@ -45,17 +46,23 @@ function encrypt(key, iv, plainText) {
|
||||
|
||||
const encryptedData = Buffer.concat([cipher.update(digestWithPayload), cipher.final()]);
|
||||
|
||||
return encryptedData.toString('base64');
|
||||
const encryptedDataWithIv = Buffer.concat([iv, encryptedData]);
|
||||
|
||||
return encryptedDataWithIv.toString('base64');
|
||||
}
|
||||
|
||||
function decrypt(key, iv, cipherText) {
|
||||
function decrypt(key, cipherText, ivLength = 13) {
|
||||
if (!key) {
|
||||
return "[protected]";
|
||||
}
|
||||
|
||||
const cipherTextBufferWithIv = Buffer.from(cipherText, 'base64');
|
||||
const iv = cipherTextBufferWithIv.slice(0, ivLength);
|
||||
|
||||
const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength);
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', pad(key), pad(iv));
|
||||
|
||||
const cipherTextBuffer = Buffer.from(cipherText, 'base64');
|
||||
const decryptedBytes = Buffer.concat([decipher.update(cipherTextBuffer), decipher.final()]);
|
||||
|
||||
const digest = decryptedBytes.slice(0, 4);
|
||||
@@ -70,8 +77,8 @@ function decrypt(key, iv, cipherText) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
function decryptString(dataKey, iv, cipherText) {
|
||||
const buffer = decrypt(dataKey, iv, cipherText);
|
||||
function decryptString(dataKey, cipherText) {
|
||||
const buffer = decrypt(dataKey, cipherText);
|
||||
|
||||
const str = buffer.toString('utf-8');
|
||||
|
||||
@@ -84,26 +91,8 @@ function decryptString(dataKey, iv, cipherText) {
|
||||
return str;
|
||||
}
|
||||
|
||||
function noteTitleIv(iv) {
|
||||
if (!iv) {
|
||||
throw new Error("Empty iv!");
|
||||
}
|
||||
|
||||
return "0" + iv;
|
||||
}
|
||||
|
||||
function noteContentIv(iv) {
|
||||
if (!iv) {
|
||||
throw new Error("Empty iv!");
|
||||
}
|
||||
|
||||
return "1" + iv;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
encrypt,
|
||||
decrypt,
|
||||
decryptString,
|
||||
noteTitleIv,
|
||||
noteContentIv
|
||||
decryptString
|
||||
};
|
||||
+11
-12
@@ -47,8 +47,8 @@ async function getRootCalendarNote() {
|
||||
return rootNote;
|
||||
}
|
||||
|
||||
async function getYearNote(dateTimeStr, rootNote) {
|
||||
const yearStr = dateTimeStr.substr(0, 4);
|
||||
async function getYearNote(dateStr, rootNote) {
|
||||
const yearStr = dateStr.substr(0, 4);
|
||||
|
||||
let yearNote = await attributeService.getNoteWithLabel(YEAR_LABEL, yearStr);
|
||||
|
||||
@@ -66,19 +66,19 @@ async function getYearNote(dateTimeStr, rootNote) {
|
||||
return yearNote;
|
||||
}
|
||||
|
||||
async function getMonthNote(dateTimeStr, rootNote) {
|
||||
const monthStr = dateTimeStr.substr(0, 7);
|
||||
const monthNumber = dateTimeStr.substr(5, 2);
|
||||
async function getMonthNote(dateStr, rootNote) {
|
||||
const monthStr = dateStr.substr(0, 7);
|
||||
const monthNumber = dateStr.substr(5, 2);
|
||||
|
||||
let monthNote = await attributeService.getNoteWithLabel(MONTH_LABEL, monthStr);
|
||||
|
||||
if (!monthNote) {
|
||||
const yearNote = await getYearNote(dateTimeStr, rootNote);
|
||||
const yearNote = await getYearNote(dateStr, rootNote);
|
||||
|
||||
monthNote = await getNoteStartingWith(yearNote.noteId, monthNumber);
|
||||
|
||||
if (!monthNote) {
|
||||
const dateObj = dateUtils.parseDate(dateTimeStr);
|
||||
const dateObj = dateUtils.parseLocalDate(dateStr);
|
||||
|
||||
const noteTitle = monthNumber + " - " + MONTHS[dateObj.getMonth()];
|
||||
|
||||
@@ -92,21 +92,20 @@ async function getMonthNote(dateTimeStr, rootNote) {
|
||||
return monthNote;
|
||||
}
|
||||
|
||||
async function getDateNote(dateTimeStr) {
|
||||
async function getDateNote(dateStr) {
|
||||
const rootNote = await getRootCalendarNote();
|
||||
|
||||
const dateStr = dateTimeStr.substr(0, 10);
|
||||
const dayNumber = dateTimeStr.substr(8, 2);
|
||||
const dayNumber = dateStr.substr(8, 2);
|
||||
|
||||
let dateNote = await attributeService.getNoteWithLabel(DATE_LABEL, dateStr);
|
||||
|
||||
if (!dateNote) {
|
||||
const monthNote = await getMonthNote(dateTimeStr, rootNote);
|
||||
const monthNote = await getMonthNote(dateStr, rootNote);
|
||||
|
||||
dateNote = await getNoteStartingWith(monthNote.noteId, dayNumber);
|
||||
|
||||
if (!dateNote) {
|
||||
const dateObj = dateUtils.parseDate(dateTimeStr);
|
||||
const dateObj = dateUtils.parseLocalDate(dateStr);
|
||||
|
||||
const noteTitle = dayNumber + " - " + DAYS[dateObj.getDay()];
|
||||
|
||||
|
||||
@@ -2,6 +2,16 @@ function nowDate() {
|
||||
return dateStr(new Date());
|
||||
}
|
||||
|
||||
function nowLocalDate() {
|
||||
const date = new Date();
|
||||
|
||||
return date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate());
|
||||
}
|
||||
|
||||
function pad(num) {
|
||||
return num <= 9 ? `0${num}` : `${num}`;
|
||||
}
|
||||
|
||||
function dateStr(date) {
|
||||
return date.toISOString();
|
||||
}
|
||||
@@ -25,14 +35,23 @@ function parseDate(str) {
|
||||
return parseDateTime(datePart + "T12:00:00.000Z");
|
||||
}
|
||||
|
||||
function parseLocalDate(str) {
|
||||
const datePart = str.substr(0, 10);
|
||||
|
||||
// not specifying the timezone and specifying the time means Date.parse() will use the local timezone
|
||||
return parseDateTime(datePart + " 12:00:00.000");
|
||||
}
|
||||
|
||||
function getDateTimeForFile() {
|
||||
return new Date().toISOString().substr(0, 19).replace(/:/g, '');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
nowDate,
|
||||
nowLocalDate,
|
||||
dateStr,
|
||||
parseDate,
|
||||
parseDateTime,
|
||||
parseLocalDate,
|
||||
getDateTimeForFile
|
||||
};
|
||||
@@ -1,13 +1,10 @@
|
||||
"use strict";
|
||||
|
||||
const sanitize = require("sanitize-filename");
|
||||
const repository = require("../../services/repository");
|
||||
const utils = require('../../services/utils');
|
||||
const repository = require("../repository");
|
||||
const utils = require('../utils');
|
||||
|
||||
async function exportToOpml(branch, res) {
|
||||
const note = await branch.getNote();
|
||||
const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title;
|
||||
const sanitizedTitle = sanitize(title);
|
||||
|
||||
async function exportNoteInner(branchId) {
|
||||
const branch = await repository.getBranch(branchId);
|
||||
@@ -31,7 +28,9 @@ async function exportToOpml(branch, res) {
|
||||
res.write('</outline>');
|
||||
}
|
||||
|
||||
res.setHeader('Content-Disposition', 'file; filename="' + sanitizedTitle + '.opml"');
|
||||
const filename = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title + ".opml";
|
||||
|
||||
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
|
||||
res.setHeader('Content-Type', 'text/x-opml');
|
||||
|
||||
res.write(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use strict";
|
||||
|
||||
const sanitize = require("sanitize-filename");
|
||||
const TurndownService = require('turndown');
|
||||
const mimeTypes = require('mime-types');
|
||||
const html = require('html');
|
||||
const utils = require('../utils');
|
||||
|
||||
async function exportSingleNote(branch, format, res) {
|
||||
const note = await branch.getNote();
|
||||
@@ -20,6 +20,10 @@ async function exportSingleNote(branch, format, res) {
|
||||
|
||||
if (note.type === 'text') {
|
||||
if (format === 'html') {
|
||||
if (!note.content.toLowerCase().includes("<html")) {
|
||||
note.content = '<html><head><meta charset="utf-8"></head><body>' + note.content + '</body></html>';
|
||||
}
|
||||
|
||||
payload = html.prettyPrint(note.content, {indent_size: 2});
|
||||
extension = 'html';
|
||||
mime = 'text/html';
|
||||
@@ -42,11 +46,9 @@ async function exportSingleNote(branch, format, res) {
|
||||
mime = 'application/json';
|
||||
}
|
||||
|
||||
const name = sanitize(note.title);
|
||||
const filename = note.title + "." + extension;
|
||||
|
||||
console.log(name, extension, mime);
|
||||
|
||||
res.setHeader('Content-Disposition', `file; filename="${name}.${extension}"`);
|
||||
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
|
||||
res.setHeader('Content-Type', mime + '; charset=UTF-8');
|
||||
|
||||
res.send(payload);
|
||||
|
||||
@@ -4,10 +4,11 @@ const html = require('html');
|
||||
const repository = require('../repository');
|
||||
const tar = require('tar-stream');
|
||||
const path = require('path');
|
||||
const sanitize = require("sanitize-filename");
|
||||
const mimeTypes = require('mime-types');
|
||||
const TurndownService = require('turndown');
|
||||
const packageInfo = require('../../../package.json');
|
||||
const utils = require('../utils');
|
||||
const sanitize = require("sanitize-filename");
|
||||
|
||||
/**
|
||||
* @param format - 'html' or 'markdown'
|
||||
@@ -73,11 +74,10 @@ async function exportToTar(branch, format, res) {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseFileName = branch.prefix ? (branch.prefix + ' - ' + note.title) : note.title;
|
||||
const baseFileName = sanitize(branch.prefix ? (branch.prefix + ' - ' + note.title) : note.title);
|
||||
|
||||
if (note.noteId in noteIdToMeta) {
|
||||
const sanitizedFileName = sanitize(baseFileName + ".clone");
|
||||
const fileName = getUniqueFilename(existingFileNames, sanitizedFileName);
|
||||
const fileName = getUniqueFilename(existingFileNames, baseFileName + ".clone");
|
||||
|
||||
return {
|
||||
isClone: true,
|
||||
@@ -149,6 +149,10 @@ async function exportToTar(branch, format, res) {
|
||||
|
||||
function prepareContent(note, format) {
|
||||
if (format === 'html') {
|
||||
if (!note.content.toLowerCase().includes("<html")) {
|
||||
note.content = '<html><head><meta charset="utf-8"></head><body>' + note.content + '</body></html>';
|
||||
}
|
||||
|
||||
return html.prettyPrint(note.content, {indent_size: 2});
|
||||
}
|
||||
else if (format === 'markdown') {
|
||||
@@ -219,9 +223,9 @@ async function exportToTar(branch, format, res) {
|
||||
pack.finalize();
|
||||
|
||||
const note = await branch.getNote();
|
||||
const tarFileName = sanitize((branch.prefix ? (branch.prefix + " - ") : "") + note.title);
|
||||
const tarFileName = (branch.prefix ? (branch.prefix + " - ") : "") + note.title + ".tar";
|
||||
|
||||
res.setHeader('Content-Disposition', `file; filename="${tarFileName}.tar"`);
|
||||
res.setHeader('Content-Disposition', utils.getContentDisposition(tarFileName));
|
||||
res.setHeader('Content-Type', 'application/tar');
|
||||
|
||||
pack.pipe(res);
|
||||
|
||||
@@ -12,7 +12,7 @@ async function runAttachedRelations(note, relationName, originEntity) {
|
||||
const scriptNote = await relation.getTargetNote();
|
||||
|
||||
if (scriptNote) {
|
||||
await scriptService.executeNote(scriptNote, originEntity);
|
||||
await scriptService.executeNoteNoException(scriptNote, { originEntity });
|
||||
}
|
||||
else {
|
||||
log.error(`Target note ${relation.value} of atttribute ${relation.attributeId} has not been found.`);
|
||||
@@ -30,14 +30,14 @@ eventService.subscribe(eventService.NOTE_TITLE_CHANGED, async note => {
|
||||
if (await parent.hasLabel("sorted")) {
|
||||
await treeService.sortNotesAlphabetically(parent.noteId);
|
||||
|
||||
messagingService.sendMessageToAllClients({ type: 'refresh-tree' });
|
||||
messagingService.refreshTree();
|
||||
break; // sending the message once is enough
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityName, entity }) => {
|
||||
eventService.subscribe([ eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED ], async ({ entityName, entity }) => {
|
||||
if (entityName === 'attributes') {
|
||||
await runAttachedRelations(await entity.getNote(), 'runOnAttributeChange', entity);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@ async function sendMessage(client, message) {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshTree() {
|
||||
await sendMessageToAllClients({ type: 'refresh-tree' });
|
||||
}
|
||||
|
||||
async function sendMessageToAllClients(message) {
|
||||
const jsonStr = JSON.stringify(message);
|
||||
|
||||
@@ -76,5 +80,6 @@ async function sendPing(client, lastSentSyncId) {
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
refreshTree,
|
||||
sendMessageToAllClients
|
||||
};
|
||||
+2
-12
@@ -359,18 +359,8 @@ async function deleteNote(branch) {
|
||||
const notDeletedBranches = await note.getBranches();
|
||||
|
||||
if (notDeletedBranches.length === 0) {
|
||||
// maybe a bit counter-intuitively, protected notes can be deleted also outside of protected session
|
||||
// this is because protected notes offer only confidentiality which makes some things simpler - e.g. deletion UI
|
||||
// to allow this, we just set the isDeleted flag, otherwise saving would fail because of attempt to encrypt
|
||||
// content with non-existent protected session key
|
||||
// we don't reset content here, that's postponed and done later to give the user a chance to correct a mistake
|
||||
await sql.execute("UPDATE notes SET isDeleted = 1 WHERE noteId = ?", [note.noteId]);
|
||||
// need to manually trigger sync since it's not taken care of by note save
|
||||
await syncTableService.addNoteSync(note.noteId);
|
||||
|
||||
for (const noteRevision of await note.getRevisions()) {
|
||||
await noteRevision.save();
|
||||
}
|
||||
note.isDeleted = true;
|
||||
await note.save();
|
||||
|
||||
for (const childBranch of await note.getChildBranches()) {
|
||||
await deleteNote(childBranch);
|
||||
|
||||
@@ -24,7 +24,6 @@ async function initSyncedOptions(username, password) {
|
||||
|
||||
// passwordEncryptionService expects these options to already exist
|
||||
await optionService.createOption('encryptedDataKey', '', true);
|
||||
await optionService.createOption('encryptedDataKeyIv', '', true);
|
||||
|
||||
await passwordEncryptionService.setDataKey(password, utils.randomSecureToken(16), true);
|
||||
}
|
||||
@@ -50,6 +49,10 @@ async function initNotSyncedOptions(initialized, startNotePath = 'root', syncSer
|
||||
await optionService.createOption('syncServerTimeout', 5000, false);
|
||||
await optionService.createOption('syncProxy', syncProxy, false);
|
||||
|
||||
await optionService.createOption('mainFontSize', '100', false);
|
||||
await optionService.createOption('treeFontSize', '100', false);
|
||||
await optionService.createOption('detailFontSize', '110', false);
|
||||
|
||||
await optionService.createOption('initialized', initialized ? 'true' : 'false', false);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,13 +14,7 @@ async function verifyPassword(password) {
|
||||
async function setDataKey(password, plainTextDataKey) {
|
||||
const passwordDerivedKey = await myScryptService.getPasswordDerivedKey(password);
|
||||
|
||||
const encryptedDataKeyIv = utils.randomString(16);
|
||||
|
||||
await optionService.setOption('encryptedDataKeyIv', encryptedDataKeyIv);
|
||||
|
||||
const buffer = Buffer.from(plainTextDataKey);
|
||||
|
||||
const newEncryptedDataKey = dataEncryptionService.encrypt(passwordDerivedKey, encryptedDataKeyIv, buffer);
|
||||
const newEncryptedDataKey = dataEncryptionService.encrypt(passwordDerivedKey, plainTextDataKey, 16);
|
||||
|
||||
await optionService.setOption('encryptedDataKey', newEncryptedDataKey);
|
||||
}
|
||||
@@ -28,10 +22,9 @@ async function setDataKey(password, plainTextDataKey) {
|
||||
async function getDataKey(password) {
|
||||
const passwordDerivedKey = await myScryptService.getPasswordDerivedKey(password);
|
||||
|
||||
const encryptedDataKeyIv = await optionService.getOption('encryptedDataKeyIv');
|
||||
const encryptedDataKey = await optionService.getOption('encryptedDataKey');
|
||||
|
||||
const decryptedDataKey = dataEncryptionService.decrypt(passwordDerivedKey, encryptedDataKeyIv, encryptedDataKey);
|
||||
const decryptedDataKey = dataEncryptionService.decrypt(passwordDerivedKey, encryptedDataKey, 16);
|
||||
|
||||
return decryptedDataKey;
|
||||
}
|
||||
|
||||
@@ -38,9 +38,7 @@ function decryptNoteTitle(noteId, encryptedTitle) {
|
||||
const dataKey = getDataKey();
|
||||
|
||||
try {
|
||||
const iv = dataEncryptionService.noteTitleIv(noteId);
|
||||
|
||||
return dataEncryptionService.decryptString(dataKey, iv, encryptedTitle);
|
||||
return dataEncryptionService.decryptString(dataKey, encryptedTitle);
|
||||
}
|
||||
catch (e) {
|
||||
e.message = `Cannot decrypt note title for noteId=${noteId}: ` + e.message;
|
||||
@@ -57,17 +55,15 @@ function decryptNote(note) {
|
||||
|
||||
try {
|
||||
if (note.title) {
|
||||
note.title = dataEncryptionService.decryptString(dataKey, dataEncryptionService.noteTitleIv(note.noteId), note.title);
|
||||
note.title = dataEncryptionService.decryptString(dataKey, note.title);
|
||||
}
|
||||
|
||||
if (note.content) {
|
||||
const contentIv = dataEncryptionService.noteContentIv(note.noteId);
|
||||
|
||||
if (note.type === 'file') {
|
||||
note.content = dataEncryptionService.decrypt(dataKey, contentIv, note.content);
|
||||
if (note.type === 'file' || note.type === 'image') {
|
||||
note.content = dataEncryptionService.decrypt(dataKey, note.content);
|
||||
}
|
||||
else {
|
||||
note.content = dataEncryptionService.decryptString(dataKey, contentIv, note.content);
|
||||
note.content = dataEncryptionService.decryptString(dataKey, note.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,26 +87,26 @@ function decryptNoteRevision(hist) {
|
||||
}
|
||||
|
||||
if (hist.title) {
|
||||
hist.title = dataEncryptionService.decryptString(dataKey, dataEncryptionService.noteTitleIv(hist.noteRevisionId), hist.title);
|
||||
hist.title = dataEncryptionService.decryptString(dataKey, hist.title);
|
||||
}
|
||||
|
||||
if (hist.content) {
|
||||
hist.content = dataEncryptionService.decryptString(dataKey, dataEncryptionService.noteContentIv(hist.noteRevisionId), hist.content);
|
||||
hist.content = dataEncryptionService.decryptString(dataKey, hist.content);
|
||||
}
|
||||
}
|
||||
|
||||
function encryptNote(note) {
|
||||
const dataKey = getDataKey();
|
||||
|
||||
note.title = dataEncryptionService.encrypt(dataKey, dataEncryptionService.noteTitleIv(note.noteId), note.title);
|
||||
note.content = dataEncryptionService.encrypt(dataKey, dataEncryptionService.noteContentIv(note.noteId), note.content);
|
||||
note.title = dataEncryptionService.encrypt(dataKey, note.title);
|
||||
note.content = dataEncryptionService.encrypt(dataKey, note.content);
|
||||
}
|
||||
|
||||
function encryptNoteRevision(revision) {
|
||||
const dataKey = getDataKey();
|
||||
|
||||
revision.title = dataEncryptionService.encrypt(dataKey, dataEncryptionService.noteTitleIv(revision.noteRevisionId), revision.title);
|
||||
revision.content = dataEncryptionService.encrypt(dataKey, dataEncryptionService.noteContentIv(revision.noteRevisionId), revision.content);
|
||||
revision.title = dataEncryptionService.encrypt(dataKey, revision.title);
|
||||
revision.content = dataEncryptionService.encrypt(dataKey, revision.content);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -47,11 +47,6 @@ async function getBranch(branchId) {
|
||||
return await getEntity("SELECT * FROM branches WHERE branchId = ?", [branchId]);
|
||||
}
|
||||
|
||||
/** @returns {Image|null} */
|
||||
async function getImage(imageId) {
|
||||
return await getEntity("SELECT * FROM images WHERE imageId = ?", [imageId]);
|
||||
}
|
||||
|
||||
/** @returns {Attribute|null} */
|
||||
async function getAttribute(attributeId) {
|
||||
return await getEntity("SELECT * FROM attributes WHERE attributeId = ?", [attributeId]);
|
||||
@@ -62,6 +57,11 @@ async function getOption(name) {
|
||||
return await getEntity("SELECT * FROM options WHERE name = ?", [name]);
|
||||
}
|
||||
|
||||
/** @returns {Link|null} */
|
||||
async function getLink(linkId) {
|
||||
return await getEntity("SELECT * FROM links WHERE linkId = ?", [linkId]);
|
||||
}
|
||||
|
||||
async function updateEntity(entity) {
|
||||
const entityName = entity.constructor.entityName;
|
||||
const primaryKeyName = entity.constructor.primaryKeyName;
|
||||
@@ -122,9 +122,9 @@ module.exports = {
|
||||
getEntity,
|
||||
getNote,
|
||||
getBranch,
|
||||
getImage,
|
||||
getAttribute,
|
||||
getOption,
|
||||
getLink,
|
||||
updateEntity,
|
||||
setEntityConstructor
|
||||
};
|
||||
@@ -7,7 +7,6 @@ const RESOURCE_DIR = path.resolve(__dirname, "../..");
|
||||
// where "trilium" executable is
|
||||
const ELECTRON_APP_ROOT_DIR = path.resolve(RESOURCE_DIR, "../..");
|
||||
const DB_INIT_DIR = path.resolve(RESOURCE_DIR, "db");
|
||||
const APP_PNG_ICON_DIR = path.resolve(RESOURCE_DIR, "src/public/images/app-icons/png");
|
||||
|
||||
if (!fs.existsSync(DB_INIT_DIR)) {
|
||||
log.error("Could not find DB initialization directory: " + DB_INIT_DIR);
|
||||
@@ -25,6 +24,5 @@ module.exports = {
|
||||
RESOURCE_DIR,
|
||||
MIGRATIONS_DIR,
|
||||
DB_INIT_DIR,
|
||||
ELECTRON_APP_ROOT_DIR,
|
||||
APP_PNG_ICON_DIR
|
||||
ELECTRON_APP_ROOT_DIR
|
||||
};
|
||||
@@ -17,7 +17,7 @@ async function runNotesWithLabel(runAttrValue) {
|
||||
AND notes.isDeleted = 0`, [runAttrValue]);
|
||||
|
||||
for (const note of notes) {
|
||||
scriptService.executeNote(note, note);
|
||||
scriptService.executeNoteNoException(note, { originEntity: note });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+37
-18
@@ -5,37 +5,48 @@ const cls = require('./cls');
|
||||
const sourceIdService = require('./source_id');
|
||||
const log = require('./log');
|
||||
|
||||
async function executeNote(note, originEntity) {
|
||||
if (!note.isJavaScript() || !note.isContentAvailable) {
|
||||
async function executeNote(note, apiParams) {
|
||||
if (!note.isJavaScript() || note.getScriptEnv() !== 'backend' || !note.isContentAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bundle = await getScriptBundle(note);
|
||||
|
||||
await executeBundle(bundle, note, originEntity);
|
||||
await executeBundle(bundle, apiParams);
|
||||
}
|
||||
|
||||
async function executeBundle(bundle, startNote, originEntity = null) {
|
||||
if (!startNote) {
|
||||
async function executeNoteNoException(note, apiParams) {
|
||||
try {
|
||||
await executeNote(note, apiParams);
|
||||
}
|
||||
catch (e) {
|
||||
// just swallow, exception is logged already in executeNote
|
||||
}
|
||||
}
|
||||
|
||||
async function executeBundle(bundle, apiParams = {}) {
|
||||
if (!apiParams.startNote) {
|
||||
// this is the default case, the only exception is when we want to preserve frontend startNote
|
||||
startNote = bundle.note;
|
||||
apiParams.startNote = bundle.note;
|
||||
}
|
||||
|
||||
// last \r\n is necessary if script contains line comment on its last line
|
||||
const script = "async function() {\r\n" + bundle.script + "\r\n}";
|
||||
|
||||
const ctx = new ScriptContext(startNote, bundle.allNotes, originEntity);
|
||||
const ctx = new ScriptContext(bundle.allNotes, apiParams);
|
||||
|
||||
try {
|
||||
if (await bundle.note.hasLabel('manualTransactionHandling')) {
|
||||
return await execute(ctx, script, '');
|
||||
return await execute(ctx, script);
|
||||
}
|
||||
else {
|
||||
return await sql.transactional(async () => await execute(ctx, script, ''));
|
||||
return await sql.transactional(async () => await execute(ctx, script));
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
log.error(`Execution of script "${bundle.note.title}" (${bundle.note.noteId}) failed with error: ${e.message}`);
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,11 +68,11 @@ async function executeScript(script, params, startNoteId, currentNoteId, originE
|
||||
return await executeBundle(bundle, startNote, originEntity);
|
||||
}
|
||||
|
||||
async function execute(ctx, script, paramsStr) {
|
||||
async function execute(ctx, script, params = []) {
|
||||
// scripts run as "server" sourceId so clients recognize the changes as "foreign" and update themselves
|
||||
cls.namespace.set('sourceId', sourceIdService.getCurrentSourceId());
|
||||
|
||||
return await (function() { return eval(`const apiContext = this;\r\n(${script}\r\n)(${paramsStr})`); }.call(ctx));
|
||||
return await (function() { return eval(`const apiContext = this;\r\n(${script}\r\n)()`); }.call(ctx));
|
||||
}
|
||||
|
||||
function getParams(params) {
|
||||
@@ -79,6 +90,19 @@ function getParams(params) {
|
||||
}).join(",");
|
||||
}
|
||||
|
||||
async function getScriptBundleForFrontend(note) {
|
||||
const bundle = await getScriptBundle(note);
|
||||
|
||||
// for frontend we return just noteIds because frontend needs to use its own entity instances
|
||||
bundle.noteId = bundle.note.noteId;
|
||||
delete bundle.note;
|
||||
|
||||
bundle.allNoteIds = bundle.allNotes.map(note => note.noteId);
|
||||
delete bundle.allNotes;
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
async function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = []) {
|
||||
if (!note.isContentAvailable) {
|
||||
return;
|
||||
@@ -153,14 +177,9 @@ function sanitizeVariableName(str) {
|
||||
return str.replace(/[^a-z0-9_]/gim, "");
|
||||
}
|
||||
|
||||
async function getScriptBundleForNoteId(noteId) {
|
||||
const note = await repository.getNote(noteId);
|
||||
return await getScriptBundle(note);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
executeNote,
|
||||
executeNoteNoException,
|
||||
executeScript,
|
||||
getScriptBundle,
|
||||
getScriptBundleForNoteId
|
||||
getScriptBundleForFrontend
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
const utils = require('./utils');
|
||||
const BackendScriptApi = require('./backend_script_api');
|
||||
|
||||
function ScriptContext(startNote, allNotes, originEntity = null) {
|
||||
function ScriptContext(allNotes, apiParams = {}) {
|
||||
this.modules = {};
|
||||
this.notes = utils.toObject(allNotes, note => [note.noteId, note]);
|
||||
this.apis = utils.toObject(allNotes, note => [note.noteId, new BackendScriptApi(startNote, note, originEntity)]);
|
||||
this.apis = utils.toObject(allNotes, note => [note.noteId, new BackendScriptApi(note, apiParams)]);
|
||||
this.require = moduleNoteIds => {
|
||||
return moduleName => {
|
||||
const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId));
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ async function execute(query, params = []) {
|
||||
|
||||
const milliseconds = Date.now() - startTimestamp;
|
||||
if (milliseconds >= 200) {
|
||||
log.info(`Slow query took ${milliseconds}ms: ${query}, params=${params}`);
|
||||
log.info(`Slow query took ${milliseconds}ms: ${query}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -155,6 +155,10 @@ async function dbInitialized() {
|
||||
await initDbConnection();
|
||||
}
|
||||
|
||||
dbReady.then(async () => {
|
||||
log.info("DB size: " + await sql.getValue("SELECT page_count * page_size / 1000 as size FROM pragma_page_count(), pragma_page_size()") + " KB");
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
dbReady,
|
||||
schemaExists,
|
||||
|
||||
@@ -111,6 +111,12 @@ async function sortNotesAlphabetically(parentNoteId) {
|
||||
}
|
||||
|
||||
async function setNoteToParent(noteId, prefix, parentNoteId) {
|
||||
const parentNote = await repository.getNote(parentNoteId);
|
||||
|
||||
if (parentNote && parentNote.isDeleted) {
|
||||
throw new Error(`Cannot move note to deleted parent note ${parentNoteId}`);
|
||||
}
|
||||
|
||||
// case where there might be more such branches is ignored. It's expected there should be just one
|
||||
const branch = await repository.getEntity("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ? AND prefix = ?", [noteId, prefix]);
|
||||
|
||||
@@ -126,6 +132,12 @@ async function setNoteToParent(noteId, prefix, parentNoteId) {
|
||||
await branch.save();
|
||||
}
|
||||
else if (parentNoteId) {
|
||||
const note = await repository.getNote(noteId);
|
||||
|
||||
if (note.isDeleted) {
|
||||
throw new Error(`Cannot create a branch for ${noteId} which is deleted.`);
|
||||
}
|
||||
|
||||
await new Branch({
|
||||
noteId: noteId,
|
||||
parentNoteId: parentNoteId,
|
||||
|
||||
+20
-1
@@ -4,6 +4,7 @@ const crypto = require('crypto');
|
||||
const randtoken = require('rand-token').generator({source: 'crypto'});
|
||||
const unescape = require('unescape');
|
||||
const escape = require('escape-html');
|
||||
const sanitize = require("sanitize-filename");
|
||||
|
||||
function newEntityId() {
|
||||
return randomString(12);
|
||||
@@ -127,6 +128,22 @@ function crash() {
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeFilenameForHeader(filename) {
|
||||
let sanitizedFilename = sanitize(filename);
|
||||
|
||||
if (sanitizedFilename.trim().length === 0) {
|
||||
sanitizedFilename = "file";
|
||||
}
|
||||
|
||||
return encodeURIComponent(sanitizedFilename)
|
||||
}
|
||||
|
||||
function getContentDisposition(filename) {
|
||||
const sanitizedFilename = sanitizeFilenameForHeader(filename);
|
||||
|
||||
return `file; filename="${sanitizedFilename}"; filename*=UTF-8''${sanitizedFilename}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
randomSecureToken,
|
||||
randomString,
|
||||
@@ -147,5 +164,7 @@ module.exports = {
|
||||
intersection,
|
||||
union,
|
||||
escapeRegExp,
|
||||
crash
|
||||
crash,
|
||||
sanitizeFilenameForHeader,
|
||||
getContentDisposition
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="theme-<%= theme %>">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Trilium Notes</title>
|
||||
</head>
|
||||
<body class="desktop">
|
||||
<body class="desktop theme-<%= theme %>" style="--main-font-size: <%= mainFontSize %>%; --tree-font-size: <%= treeFontSize %>%; --detail-font-size: <%= detailFontSize %>%;">
|
||||
<div id="container" style="display: none; grid-template-columns: minmax(<%= leftPaneMinWidth %>px, <%= leftPaneWidthPercent %>fr) <%= rightPaneWidthPercent %>fr">
|
||||
<div id="header" class="hide-toggle">
|
||||
<div id="history-navigation" style="display: none;">
|
||||
@@ -75,8 +75,8 @@
|
||||
<input type="file" id="import-upload" style="display: none" />
|
||||
|
||||
<div id="search-box">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<input name="search-text" placeholder="Search text, labels" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off">
|
||||
<div style="display: flex; align-items: center; flex-wrap: wrap;">
|
||||
<input name="search-text" id="search-text" placeholder="Search text, labels" style="flex-grow: 100; margin-left: 5px; margin-right: 5px; flex-basis: 5em; min-width: 0;" autocomplete="off">
|
||||
<button id="do-search-button" class="btn btn-sm icon-button jam jam-search" title="Search (enter)"></button>
|
||||
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
maxSyncIdAtLoad: <%= maxSyncIdAtLoad %>,
|
||||
instanceName: '<%= instanceName %>'
|
||||
};
|
||||
window.appCssNoteIds = <%- JSON.stringify(appCssNoteIds) %>;
|
||||
</script>
|
||||
|
||||
<!-- Required for correct loading of scripts in Electron -->
|
||||
@@ -247,9 +248,5 @@
|
||||
// final form which is pretty ugly.
|
||||
$("#container").show();
|
||||
</script>
|
||||
|
||||
<style type="text/css">
|
||||
<%= appCss %>
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<div id="note-detail-file" class="note-detail-component">
|
||||
<table id="file-table">
|
||||
<tr>
|
||||
<th>Note ID:</th>
|
||||
<td id="file-note-id"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Original file name:</th>
|
||||
<td id="file-filename"></td>
|
||||
@@ -19,7 +23,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<td colspan="2">
|
||||
<button id="file-download" class="btn btn-primary" type="button">Download</button>
|
||||
|
||||
<button id="file-open" class="btn btn-primary" type="button">Open</button>
|
||||
|
||||
@@ -40,11 +40,7 @@
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="theme-select">Theme</label>
|
||||
<select class="form-control" id="theme-select">
|
||||
<option value="white">White</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="black">Black</option>
|
||||
</select>
|
||||
<select class="form-control" id="theme-select"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -55,6 +51,46 @@
|
||||
|
||||
<p>Zooming can be controlled with CTRL-+ and CTRL-= shortcuts as well.</p>
|
||||
|
||||
<h3>Font sizes</h3>
|
||||
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-4">
|
||||
<label for="main-font-size">Main font size</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="main-font-size" min="50" max="200" step="10"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<label for="tree-font-size">Note tree font size</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="tree-font-size" min="50" max="200" step="10"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<label for="detail-font-size">Note detail font size</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="detail-font-size" min="50" max="200" step="10"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Note that tree and detail font sizing is relative to the main font size setting.</p>
|
||||
|
||||
<h3>Left pane sizing</h3>
|
||||
|
||||
<div class="form-group">
|
||||
|
||||
+4
-3
@@ -4,6 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>Login</title>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/app-icons/ios/apple-touch-icon.png">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
@@ -71,7 +72,7 @@
|
||||
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
||||
}
|
||||
</script>
|
||||
|
||||
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
</body>
|
||||
|
||||
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>Trilium Notes</title>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/app-icons/ios/apple-touch-icon.png">
|
||||
</head>
|
||||
<body class="mobile">
|
||||
<div class="row" id="container-row" style="display: none;">
|
||||
@@ -101,7 +102,7 @@
|
||||
<script type="text/javascript">
|
||||
// we hide container initally because otherwise it is rendered first without CSS and then flickers into
|
||||
// final form which is pretty ugly.
|
||||
$("#container-row").show();
|
||||
</script>
|
||||
</body>
|
||||
$("#container-row").show();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+2
-1
@@ -7,7 +7,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div id="setup-dialog" class="col-md-12 col-lg-8 col-xl-6 mx-auto" style="padding-top: 25px;">
|
||||
<div id="setup-dialog" class="col-md-12 col-lg-8 col-xl-6 mx-auto" style="padding-top: 25px; font-size: larger; display: none;">
|
||||
<h1>Trilium Notes setup</h1>
|
||||
|
||||
<div class="alert alert-warning" id="alert" style="display: none;">
|
||||
@@ -128,6 +128,7 @@
|
||||
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
|
||||
|
||||
<script src="libraries/jquery.min.js"></script>
|
||||
<script src="libraries/jquery.hotkeys.js"></script>
|
||||
|
||||
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="libraries/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
@@ -20,6 +20,12 @@ const messagingService = require('./services/messaging');
|
||||
const utils = require('./services/utils');
|
||||
const sqlInit = require('./services/sql_init');
|
||||
const port = require('./services/port');
|
||||
const semver = require('semver');
|
||||
|
||||
if (!semver.satisfies(process.version, ">=10.5.0")) {
|
||||
console.error("Trilium only supports node.js 10.5 and later");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let httpServer;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user