Compare commits

...

88 Commits

Author SHA1 Message Date
azivner
1a9a49b739 release 0.28.3 2019-01-22 23:01:32 +01:00
azivner
2ac560c56e text note should change its icon after having new note inserted into, fixes #361 2019-01-22 21:21:44 +01:00
azivner
c23387c0fb make search dialog more responsive in narrow sidebar, fixes #367 2019-01-22 20:54:00 +01:00
azivner
0ff250fe15 font sizes were not created for new documents - fix plus migration for existing ones 2019-01-22 20:34:45 +01:00
azivner
52b1e58b26 make tree selection more visually consistent 2019-01-22 20:23:54 +01:00
azivner
484715e440 fix icon in desktop linux build, fixes #372 2019-01-22 19:49:33 +01:00
azivner
9d42c3d802 release 0.28.2 2019-01-21 22:55:12 +01:00
azivner
c654172d33 auto fixer for "undeleted branch of deleted note" consistency check 2019-01-21 22:51:49 +01:00
azivner
e17b26c883 revert consistency checks refactoring for now 2019-01-21 22:46:27 +01:00
azivner
24d02d9cf5 fix in task manager script to not error out so much on deletion 2019-01-21 22:05:37 +01:00
azivner
7208a311ac check that note is not deleted before creating a branch 2019-01-21 21:55:40 +01:00
azivner
ad7355372b issue template 2019-01-21 19:55:07 +01:00
azivner
f18b5babad uncommented issue template 2019-01-20 16:47:49 +01:00
azivner
9831ec0ca9 added issue template 2019-01-20 16:46:56 +01:00
azivner
596544eca3 fix setNoteToParent API method, closes #360 2019-01-20 16:33:09 +01:00
azivner
1f853024ee more autofixers WIP 2019-01-19 09:57:51 +01:00
azivner
0308b13460 expose app info on the backend script api #345 2019-01-18 23:57:08 +01:00
azivner
06b8a82f70 refactoring of consistency checks + some auto fixers 2019-01-18 19:32:59 +01:00
azivner
91ca07929d before we clone the note we must make sure it's not deleted 2019-01-17 23:24:59 +01:00
azivner
afabaa5fdb workaround for hidden last line on mobile 2019-01-17 21:13:53 +01:00
azivner
a6fd3fa77c release 0.28.1-beta 2019-01-17 00:03:17 +01:00
azivner
58a2c08dcd release 0.28.1-beta 2019-01-16 23:58:10 +01:00
azivner
299bbff2f4 fix too large font on mobile 2019-01-16 23:57:09 +01:00
azivner
19d8947123 runOnNoteChange fires also on the frontend, closes #340 2019-01-16 22:52:32 +01:00
azivner
35edce7523 fix crazy clipboard operation on mac setup, closes #348 2019-01-16 22:03:30 +01:00
azivner
bc4cec69a5 use local dates to create day notes, closes #337 2019-01-15 23:46:01 +01:00
azivner
cce8c1b674 create top level note and collapse tree now work relative to hoisted note, closes #343 2019-01-15 20:30:54 +01:00
azivner
aa58788769 added basic DB size diagnostic log 2019-01-15 20:00:24 +01:00
azivner
6c62ab7a52 removing logging params for slow queries 2019-01-15 19:36:04 +01:00
azivner
d6ab638b30 fix file names in github release script 2019-01-15 00:10:47 +01:00
azivner
bd4db406de release 0.28.0-beta 2019-01-14 23:51:55 +01:00
azivner
e50f9cd0a3 fix device detection in setup 2019-01-14 23:50:45 +01:00
azivner
2b64cbce2c added dark theme (to have same set of themes as before) 2019-01-13 23:25:30 +01:00
azivner
2797c942ab changes to options dialog to allow configuring font size, closes #326 2019-01-13 22:03:06 +01:00
azivner
f1c3278874 store font size in options #326 2019-01-13 21:27:32 +01:00
azivner
424c22dcde split UI to 3 different font sizes #326 2019-01-13 21:16:51 +01:00
azivner
5c223dfd12 all fonts are relative so it's easier to scale them properly 2019-01-13 21:04:08 +01:00
azivner
6a3e7a5a8e generate css classes for each mime type, #328 2019-01-13 20:14:33 +01:00
azivner
f88cdac000 fixes for black theme for relation map and code notes 2019-01-13 20:03:28 +01:00
azivner
eeead90f32 improved theme support using CSS variables, #328 2019-01-13 18:57:46 +01:00
azivner
b607857409 fix weight tracker demo to use relations instead of hardcoded noteId, fixes #329 2019-01-13 12:21:17 +01:00
azivner
9268f88bc3 frontend scripts now have startNote, currentNote and targetNote as NoteShort entities which e.g. provides easy access to relations/labels 2019-01-13 12:16:05 +01:00
azivner
f7f0560a9f simplification of script bundles on backend 2019-01-13 11:56:50 +01:00
azivner
3d8905207e fixed export with non-ASCII characters in note title, fixes #285, #331 2019-01-13 10:22:17 +01:00
azivner
dbc312010b update fancytree to 2.30.2 2019-01-13 09:24:00 +01:00
azivner
b115a7cf19 delete note through its entity instead of manually with SQL, closes #303 2019-01-13 00:24:51 +01:00
azivner
348562352c fixes in ASAR build and zipping 2019-01-12 19:48:45 +01:00
azivner
70fd917e7c build desktop versions into ASAR, closes #271 2019-01-12 00:05:13 +01:00
zadam
d2b60764cd Merge pull request #289 from perissology/electron-builder
use electron-builder for dev install
2019-01-11 23:54:11 +01:00
azivner
581b1fdaa5 trigger runOnAttributeChange, runOnNoteChange on entity deletions as well 2019-01-11 23:43:22 +01:00
azivner
3c19a712c0 active protected state button has darker background and is disabled 2019-01-11 23:29:56 +01:00
azivner
cc27f16088 store iv directly in the respective columns 2019-01-11 23:04:51 +01:00
azivner
dffdb82288 after creating new note, it is focused and now also selected so renaming is super easy, #318 2019-01-10 22:46:08 +01:00
azivner
2b32addade release 0.27.4 2019-01-10 21:31:30 +01:00
azivner
0b251530fa open recent notes autocomplete by focus so it is then closed with blur, fixes #272 2019-01-10 21:04:06 +01:00
azivner
f5b933149a Merge remote-tracking branch 'origin/master' 2019-01-10 19:53:47 +01:00
azivner
48bbfb8bdb fix activating note by noteId when hoisting, fixes #320 2019-01-10 19:53:42 +01:00
zadam
889971c4d6 Merge pull request #312 from perissology/evernote-import
Fixes evernote import errors
2019-01-09 23:41:17 +01:00
azivner
0722494d41 fix saving JSON note with invalid JSON (previously in such a case content was not updated), fixes #307 2019-01-09 23:36:17 +01:00
azivner
4b977a3306 setup keyboard shortcuts on the setup page as well, closes #267 2019-01-09 22:08:24 +01:00
azivner
3ff3021acd shortcuts for mac should use cmd instead of ctrl, closes #290 2019-01-09 21:42:16 +01:00
azivner
99e56a9c42 make sure to save the search note before refreshing the tree 2019-01-09 19:54:32 +01:00
azivner
77279dfe16 fix anonymization 2019-01-09 19:49:02 +01:00
perissology
93f8050454 use resizedImage if image optimization fails 2019-01-09 06:29:49 -08:00
perissology
31cfede7a7 enex import: attempt to get correct mime from Buffer 2019-01-09 06:29:13 -08:00
azivner
c8ec86e537 allow refreshing saved note, closes #304 2019-01-08 23:32:03 +01:00
azivner
05aee884b6 fix saving search note content #304 2019-01-08 22:48:53 +01:00
azivner
012ba9e060 don't attempt to run protected notes outside of protected session, fixes #279 2019-01-08 21:21:49 +01:00
azivner
8e8fd88857 process only whitelisted mime types as an image, fixes #288 2019-01-08 20:45:34 +01:00
azivner
523ccdad6b reload note cache after import, closes #293 2019-01-08 20:19:41 +01:00
azivner
ded3f605be fix almost invisible buttons on options page, closes #297 2019-01-08 19:47:35 +01:00
perissology
62b44e3549 use electron-builder for npm install
This will build deps as required for electron as well as the os.

This fixes the missing binding error for sqlite3 when trying to run the
electron app after a fresh npm i
2019-01-08 06:50:48 -08:00
azivner
030d12a465 stretch sync login token validity to 5 minutes #277 2019-01-07 23:29:56 +01:00
azivner
4d15628840 Merge remote-tracking branch 'origin/master' 2019-01-07 23:17:45 +01:00
azivner
81b849898c stretch body to full window width, fixes #276 2019-01-07 23:17:12 +01:00
zadam
3824486b85 Merge pull request #275 from Lee303/Dockerfile-dependency-fix
Update Dockerfile
2019-01-07 22:49:27 +01:00
Lee Spottiswood
081ab00a0a Update Dockerfile 2019-01-07 21:21:23 +00:00
zadam
04f6af5c9a Merge pull request #270 from svenefftinge/master
Make contributions easier
2019-01-07 21:46:02 +01:00
Sven Efftinge
4dc1f1f6eb Added contribute section and gitpod config 2019-01-07 12:52:02 +00:00
azivner
3930a02123 tree now uses standard font size which effectively makes it a bit larger 2019-01-06 20:59:19 +01:00
azivner
3112de105e fancytree selection/hover colors are shades of gray, border is rounded 2019-01-06 18:58:12 +01:00
azivner
3b8d7b8fba release 0.27.3 2019-01-05 22:48:11 +01:00
azivner
9fca7f09a5 link to mobile frontend 2019-01-05 22:45:18 +01:00
azivner
fd39d6b3a9 using btn-secondary instead of btn-default since that doesn't exist in BS4 2019-01-05 21:51:27 +01:00
azivner
a103886ea5 responsive setup page 2019-01-05 21:49:40 +01:00
azivner
373408e401 fix Branch reference to parent note id after parent change 2019-01-05 19:25:22 +01:00
azivner
db44c1d8e6 border color tweaks 2019-01-05 11:49:17 +01:00
azivner
95a34c9e2d small tweaks of icon alignment 2019-01-05 11:41:09 +01:00
85 changed files with 11945 additions and 9727 deletions

7
.gitpod.yml Normal file
View File

@@ -0,0 +1,7 @@
tasks:
- before: nvm install 10 && nvm use 10
init: npm install
command: npm run start
ports:
- port: 8080
onOpen: open-preview

View File

@@ -17,6 +17,7 @@ RUN set -x \
libtool \
make \
nasm \
libpng-dev \
&& npm install --production \
&& apk del .build-dependencies

View File

@@ -1,7 +1,7 @@
# Trilium Notes
[![Join the chat at https://gitter.im/trilium-notes/Lobby](https://badges.gitter.im/trilium-notes/Lobby.svg)](https://gitter.im/trilium-notes/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases. See [screenshots](https://github.com/zadam/trilium/wiki/Screenshot-tour) for quick overview:
Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases. See [screenshots](https://github.com/zadam/trilium/wiki/Screenshot-tour) for quick overview:
![](https://raw.githubusercontent.com/wiki/zadam/trilium/images/screenshot.png)
@@ -18,6 +18,7 @@ Trilium Notes is a hierarchical note taking application with focus on building l
* [Relation maps](https://github.com/zadam/trilium/wiki/Relation-map) for visualizing notes and their relations
* [Scripting](https://github.com/zadam/trilium/wiki/Scripts) - see [Advanced showcases](https://github.com/zadam/trilium/wiki/Advanced-showcases)
* Scales well in both usability and performance upwards of 100 000 notes
* Touch optimized [mobile frontend](https://github.com/zadam/trilium/wiki/Mobile-frontend) for smartphones and tablets
* [Night theme](https://github.com/zadam/trilium/wiki/Themes)
* [Evernote](https://github.com/zadam/trilium/wiki/Evernote-import) and [Markdown import & export](https://github.com/zadam/trilium/wiki/Markdown)
@@ -33,4 +34,16 @@ Trilium is provided as either desktop application (Linux, Windows, Mac) or web a
[See wiki for complete list of documentation pages.](https://github.com/zadam/trilium/wiki/)
You can also read [Patterns of personal knowledge base](https://github.com/zadam/trilium/wiki/Patterns-of-personal-knowledge-base) to get some inspiration on how you might use Trilium.
You can also read [Patterns of personal knowledge base](https://github.com/zadam/trilium/wiki/Patterns-of-personal-knowledge-base) to get some inspiration on how you might use Trilium.
## Contribute
Use a browser based dev environment
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/zadam/trilium)
Or clone locally and run
```
npm install
npm run start
```

View File

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

View File

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

View File

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

View File

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

View File

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

3
bin/reset-local.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
./node_modules/.bin/electron-rebuild --arch=x64

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

5
issue_template.md Normal file
View File

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

15
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.27.1-beta",
"version": "0.27.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -3870,9 +3870,9 @@
}
},
"file-type": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz",
"integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU="
"version": "10.7.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-10.7.0.tgz",
"integrity": "sha512-AbaGtdWYYRaVrv2MwL/65myuRJ9j3e79e7etJ79US18QHuVlzJBcQHUH+HxDUoLtbyWRTUfLzLkGXX3pP9kfZg=="
},
"filename-regex": {
"version": "2.0.1",
@@ -5000,6 +5000,13 @@
"integrity": "sha1-FQKvMTX5BuEiyHfDHpSve3qRRsU=",
"requires": {
"file-type": "^4.1.0"
},
"dependencies": {
"file-type": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz",
"integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU="
}
}
},
"imagemin": {

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.27.2-beta",
"version": "0.28.3",
"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",
@@ -33,6 +34,7 @@
"electron-in-page-search": "1.3.2",
"express": "4.16.4",
"express-session": "1.15.6",
"file-type": "10.7.0",
"fs-extra": "7.0.1",
"get-port": "4.1.0",
"helmet": "3.15.0",
@@ -67,6 +69,7 @@
"devDependencies": {
"devtron": "1.4.0",
"electron": "4.0.1",
"electron-builder": "20.38.4",
"electron-compile": "6.4.3",
"electron-packager": "13.0.1",
"electron-rebuild": "1.8.2",

View File

@@ -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);
@@ -56,6 +67,9 @@ class Note extends Entity {
setContent(content) {
this.content = content;
// if parsing below is not successful then there's no jsonContent as opposed to still having the old unupdated ones
delete this.jsonContent;
try {
this.jsonContent = JSON.parse(this.content);
}
@@ -626,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;
}
}

View File

@@ -36,6 +36,7 @@ import hoistedNoteService from './services/hoisted_note.js';
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';
// required for CKEditor image upload plugin
window.glob.getCurrentNode = treeService.getCurrentNode;
@@ -110,28 +111,6 @@ if (utils.isElectron()) {
});
}
function exec(cmd) {
document.execCommand(cmd);
return false;
}
if (utils.isElectron() && utils.isMac()) {
utils.bindShortcut('ctrl+c', () => exec("copy"));
utils.bindShortcut('ctrl+v', () => exec('paste'));
utils.bindShortcut('ctrl+x', () => exec('cut'));
utils.bindShortcut('ctrl+a', () => exec('selectAll'));
utils.bindShortcut('ctrl+z', () => exec('undo'));
utils.bindShortcut('ctrl+y', () => exec('redo'));
utils.bindShortcut('meta+c', () => exec("copy"));
utils.bindShortcut('meta+v', () => exec('paste'));
utils.bindShortcut('meta+x', () => exec('cut'));
utils.bindShortcut('meta+a', () => exec('selectAll'));
utils.bindShortcut('meta+z', () => exec('undo'));
utils.bindShortcut('meta+y', () => exec('redo'));
}
$("#export-note-button").click(function () {
if ($(this).hasClass("disabled")) {
return;
@@ -140,6 +119,8 @@ $("#export-note-button").click(function () {
exportDialog.showDialog('single');
});
macInit.init();
treeService.showTree();
entrypoints.registerEntrypoints();

View File

@@ -6,8 +6,6 @@ const $dialog = $("#jump-to-note-dialog");
const $autoComplete = $("#jump-to-note-autocomplete");
const $showInFullTextButton = $("#show-in-full-text-button");
$dialog.on("shown.bs.modal", e => $autoComplete.focus());
async function showDialog() {
glob.activeDialog = $dialog;

View File

@@ -44,7 +44,10 @@ 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) {
@@ -59,21 +62,27 @@ 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 $body[0].classList) {
if (clazz.startsWith("theme-")) {
$body.removeClass(clazz);
}
}
$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 +92,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 {

View File

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

View File

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

View File

@@ -87,7 +87,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();
@@ -117,10 +117,6 @@ function registerEntrypoints() {
utils.bindShortcut('ctrl+f', openInPageSearch);
if (utils.isMac()) {
utils.bindShortcut('meta+f', openInPageSearch);
}
// FIXME: do we really need these at this point?
utils.bindShortcut("ctrl+shift+up", () => {
const node = treeService.getCurrentNode();
@@ -139,8 +135,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());

View File

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

View File

@@ -0,0 +1,25 @@
/**
* Mac specific initialization
*/
import utils from "./utils.js";
function init() {
if (utils.isElectron() && utils.isMac()) {
utils.bindShortcut('meta+c', () => exec("copy"));
utils.bindShortcut('meta+v', () => exec('paste'));
utils.bindShortcut('meta+x', () => exec('cut'));
utils.bindShortcut('meta+a', () => exec('selectAll'));
utils.bindShortcut('meta+z', () => exec('undo'));
utils.bindShortcut('meta+y', () => exec('redo'));
}
}
function exec(cmd) {
document.execCommand(cmd);
return false;
}
export default {
init
}

View File

@@ -28,7 +28,7 @@ function clearText($el) {
function showRecentNotes($el) {
$el.setSelectedPath("");
$el.autocomplete("val", "");
$el.autocomplete("open");
$el.focus();
}
function initNoteAutocomplete($el, options) {
@@ -61,7 +61,13 @@ function initNoteAutocomplete($el, options) {
$clearTextButton.click(() => clearText($el));
$showRecentNotesButton.click(() => showRecentNotes($el));
$showRecentNotesButton.click(e => {
showRecentNotes($el);
// this will cause the click not give focus to the "show recent notes" button
// this is important because otherwise input will lose focus immediatelly and not show the results
return false;
});
$goToSelectedNoteButton.click(() => {
if ($el.hasClass("disabled")) {

View File

@@ -131,6 +131,9 @@ async function saveNote() {
}
$savedIndicator.fadeIn();
// run async
bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteChange');
}
async function saveNoteIfChanged() {
@@ -145,8 +148,9 @@ async function saveNoteIfChanged() {
function setNoteBackgroundIfProtected(note) {
$noteDetailWrapper.toggleClass("protected", note.isProtected);
$protectButton.toggleClass("active", note.isProtected);
$protectButton.prop("disabled", note.isProtected);
$unprotectButton.toggleClass("active", !note.isProtected);
$unprotectButton.prop("disabled", !protectedSessionHolder.isProtectedSessionAvailable());
$unprotectButton.prop("disabled", !note.isProtected || !protectedSessionHolder.isProtectedSessionAvailable());
}
async function handleProtectedSession() {
@@ -283,6 +287,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
@@ -342,6 +350,7 @@ export default {
getCurrentNoteType,
getCurrentNoteId,
focusOnTitle,
focusAndSelectTitle,
saveNote,
saveNoteIfChanged,
noteChanged,

View File

@@ -1,10 +1,13 @@
import noteDetailService from "./note_detail.js";
import treeService from "./tree.js";
import infoService from './info.js';
const $searchString = $("#search-string");
const $component = $('#note-detail-search');
const $refreshButton = $('#note-detail-search-refresh-results-button');
function getContent() {
JSON.stringify({
return JSON.stringify({
searchString: $searchString.val()
});
}
@@ -25,6 +28,14 @@ function show() {
$searchString.on('input', noteDetailService.noteChanged);
}
$refreshButton.click(async () => {
await noteDetailService.saveNoteIfChanged();
treeService.reload();
infoService.showMessage('Tree has been refreshed.');
});
export default {
getContent,
show,

View File

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

View File

@@ -87,7 +87,7 @@ $searchInput.keyup(e => {
if (e && e.which === $.ui.keyCode.ENTER) {
doSearch();
}
}).focus();
});
$doSearchButton.click(() => doSearch()); // keep long form because of argument
$resetSearchButton.click(resetSearch);

View File

@@ -94,32 +94,41 @@ async function expandToNote(notePath, expandOpts) {
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
let hoistedNoteFound = false;
let parentNoteId = null;
for (const childNoteId of runPath) {
// for first node (!parentNoteId) it doesn't matter which node is found
let node = getNode(childNoteId, parentNoteId);
if (childNoteId === hoistedNoteId) {
hoistedNoteFound = true;
}
if (!node && parentNoteId) {
const parents = getNodesByNoteId(parentNoteId);
// we expand only after hoisted note since before then nodes are not actually present in the tree
if (hoistedNoteFound) {
// for first node (!parentNoteId) it doesn't matter which node is found
let node = getNode(childNoteId, parentNoteId);
for (const parent of parents) {
// force load parents. This is useful when fancytree doesn't contain recently created notes yet.
await parent.load(true);
if (!node && parentNoteId) {
const parents = getNodesByNoteId(parentNoteId);
for (const parent of parents) {
// force load parents. This is useful when fancytree doesn't contain recently created notes yet.
await parent.load(true);
}
node = getNode(childNoteId, parentNoteId);
}
node = getNode(childNoteId, parentNoteId);
}
if (!node) {
console.error(`Can't find node for noteId=${childNoteId} with parentNoteId=${parentNoteId}`);
}
if (!node) {
console.error(`Can't find node for noteId=${childNoteId} with parentNoteId=${parentNoteId}`);
}
if (childNoteId === noteId) {
return node;
}
else {
await node.setExpanded(true, expandOpts);
if (childNoteId === noteId) {
return node;
} else {
await node.setExpanded(true, expandOpts);
}
}
parentNoteId = childNoteId;
@@ -129,9 +138,12 @@ async function expandToNote(notePath, expandOpts) {
async function activateNote(notePath, noteLoadedListener) {
utils.assertArguments(notePath);
// notePath argument can contain only noteId which is not good when hoisted since
// then we need to check the whole note path
const runNotePath = await getRunPath(notePath);
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
if (hoistedNoteId !== 'root' && !notePath.includes(hoistedNoteId)) {
if (hoistedNoteId !== 'root' && !runNotePath.includes(hoistedNoteId)) {
if (!await confirmDialog.confirm("Requested note is outside of hoisted note subtree. Do you want to unhoist?")) {
return;
}
@@ -352,6 +364,7 @@ function clearSelectedNodes() {
}
async function treeInitialized() {
// - is used in mobile to indicate that we don't want to activate any note after load
if (startNotePath === '-') {
return;
}
@@ -363,7 +376,6 @@ async function treeInitialized() {
startNotePath = null;
}
// - is used in mobile to indicate that we don't want to activate any note after load
if (startNotePath) {
const node = await activateNote(startNotePath);
@@ -438,6 +450,16 @@ function initFancyTree(tree) {
$span.append(unhoistButton);
}
},
// this is done to automatically lazy load all expanded search notes after tree load
loadChildren: function(event, data) {
data.node.visit(function(subNode){
// Load all lazy/unloaded child nodes
// (which will trigger `loadChildren` recursively)
if( subNode.isUndefined() && subNode.isExpanded() ) {
subNode.load();
}
});
}
});
@@ -476,9 +498,11 @@ async function loadTree() {
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);
@@ -519,9 +543,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) {
@@ -566,7 +592,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);
@@ -597,7 +623,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 {

View File

@@ -86,7 +86,7 @@ async function prepareNode(branch) {
extraClasses: await getExtraClasses(note),
icon: await getIcon(note),
refKey: note.noteId,
expanded: (note.type !== 'search' && branch.isExpanded) || hoistedNoteId === note.noteId
expanded: branch.isExpanded || hoistedNoteId === note.noteId
};
if (note.hasChildren() || note.type === 'search') {
@@ -168,9 +168,24 @@ async function getExtraClasses(note) {
extraClasses.push(note.type);
if (note.mime) { // some notes should not have mime type (e.g. render)
extraClasses.push(getMimeTypeClass(note.mime));
}
return extraClasses.join(" ");
}
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 {
prepareTree,
prepareBranch,

View File

@@ -158,7 +158,11 @@ class TreeCache {
return;
}
treeCache.childParentToBranch[childNoteId + '-' + newParentNoteId] = treeCache.childParentToBranch[childNoteId + '-' + oldParentNoteId];
const branchId = treeCache.childParentToBranch[childNoteId + '-' + oldParentNoteId];
const branch = await this.getBranch(branchId);
branch.parentNoteId = newParentNoteId;
treeCache.childParentToBranch[childNoteId + '-' + newParentNoteId] = branchId;
delete treeCache.childParentToBranch[childNoteId + '-' + oldParentNoteId]; // this is correct because we know that oldParentId isn't same as newParentId
// remove old associations

View File

@@ -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());
}
@@ -137,7 +139,13 @@ function randomString(len) {
function bindShortcut(keyboardShortcut, handler) {
if (isDesktop()) {
if (isMac()) {
// use CMD (meta) instead of CTRL for all shortcuts
keyboardShortcut = keyboardShortcut.replace("ctrl", "meta");
}
$(document).bind('keydown', keyboardShortcut, e => {
console.log(e);
handler();
e.preventDefault();
@@ -146,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) {

View File

@@ -1,4 +1,7 @@
import utils from "./services/utils.js";
import macInit from './services/mac_init.js';
macInit.init();
function SetupModel() {
if (syncInProgress) {

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

View File

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

View File

@@ -1,3 +1,7 @@
body {
font-size: var(--main-font-size);
}
#container {
margin: 0 auto; /* center */
height: 100vh;
@@ -26,6 +30,7 @@
flex-shrink: 1;
flex-basis: 60%;
margin-top: 10px;
font-size: var(--tree-font-size);
}
#left-pane {
@@ -36,7 +41,7 @@
#header {
grid-area: header;
background-color: #f8f8f8;
background-color: var(--header-background-color);
display: flex;
align-items: center;
padding: 4px;
@@ -44,16 +49,25 @@
#header button {
padding: 1px 5px 1px 5px;
font-size: small;
font-size: smaller;
margin-bottom: 2px;
margin-top: 2px;
margin-right: 8px;
}
#history-navigation {
margin: 0 15px 0 5px;
position: relative;
top: 2px;
}
#global-buttons {
display: flex;
justify-content: space-around;
padding: 10px 0 10px 0;
margin: 0 10px 0 16px;
border: 1px solid #ccc;
border-radius: 5px;
margin: 0 20px 0 10px;
border: 1px solid #ddd;
border-radius: 7px;
}
#context-menu-container {
@@ -76,4 +90,8 @@
margin-top: 2px;
border-width: 1px;
border-style: solid;
}
#note-detail-wrapper {
font-size: var(--detail-font-size);
}

View File

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

View File

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

View File

@@ -1,7 +1,116 @@
:root {
--main-font-size: normal;
--tree-font-size: normal;
--detail-font-size: normal;
--main-background-color: white;
--main-text-color: black;
--accented-background-color: #eee;
--more-accented-background-color: #ccc;
--header-background-color: #f8f8f8;
--button-background-color: #eee;
--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;
--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;
--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);
}
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 {
@@ -10,12 +119,23 @@ body {
#note-title {
margin-left: 15px;
font-size: x-large;
margin-right: 10px;
font-size: 150%;
border: 0;
width: 5em;
flex-grow: 100;
}
ul.fancytree-container {
/* override specific size from fancytree.css */
font-family: inherit !important;
font-size: inherit !important;
}
.fancytree-title {
margin-left: 7px !important;
}
.fancytree-node:not(.fancytree-loading) .fancytree-expander {
background: none;
width: auto;
@@ -78,8 +198,6 @@ body {
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; }
@@ -106,12 +224,18 @@ body {
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);
}
@@ -129,10 +253,25 @@ 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: #ddd !important;
border-color: #555 !important;
span.fancytree-active.fancytree-focused .fancytree-title {
color: var(--active-item-text-color) !important;
background-color: var(--active-item-background-color) !important;
border-color: #ddd !important;
border-radius: 3px;
}
span.fancytree-active:not(.fancytree-focused) .fancytree-title, span.fancytree-selected .fancytree-title {
color: var(--hover-item-text-color) !important;
background-color: var(--hover-item-background-color) !important;
border-color: #ddd !important;
border-radius: 3px;
}
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: #ddd !important;
border-radius: 3px;
}
.ui-autocomplete {
@@ -155,17 +294,6 @@ span.fancytree-active:not(.fancytree-focused) .fancytree-title {
color: #337ab7 !important;
}
#header-title {
padding: 5px 20px 5px 10px;
font-size: large;
font-weight: bold;
}
#header .btn-sm {
margin-bottom: 2px;
margin-right: 8px;
}
div.ui-tooltip {
max-width: 600px;
max-height: 600px;
@@ -231,7 +359,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;
@@ -252,8 +380,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;
}
@@ -263,7 +397,7 @@ div.ui-tooltip {
.dropdown-menu kbd
{
color: black;
color: var(--muted-text-color);
border: none;
background-color: transparent;
box-shadow: none;
@@ -331,7 +465,7 @@ div.ui-tooltip {
#file-table th, #file-table td {
padding: 10px;
font-size: large;
font-size: larger;
}
#children-overview {
@@ -347,9 +481,9 @@ div.ui-tooltip {
.child-overview {
font-weight: bold;
font-size: large;
font-size: larger;
padding: 10px;
background: #f4f4f4;
background: var(--accented-background-color);
width: 150px;
height: 90px;
line-height: 2em;
@@ -363,7 +497,7 @@ div.ui-tooltip {
}
.child-overview a {
color: #444;
color: var(--muted-text-color);
}
#sql-console-query {
@@ -377,18 +511,18 @@ div.ui-tooltip {
height: 150px;
}
#history-navigation {
margin: 0 20px 0 5px;
display: flex;
.btn {
border-radius: var(--button-border-radius);
}
.btn:not(.btn-primary):not(.btn-danger) {
border-color: #bbb;
background-color: #eee;
.btn:not(.btn-primary):not(.btn-secondary):not(.btn-danger) {
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: #ccc !important;
}
#note-path-list a.current {
@@ -409,33 +543,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);
}
@@ -538,13 +645,14 @@ 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 #aaa;
color: var(--main-text-color);
border: 1px solid #ccc;
border-radius: 5px;
text-align: left;
}
@@ -572,7 +680,7 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
.algolia-autocomplete .aa-dropdown-menu {
width: 100%;
background-color: #fff;
background-color: var(--main-background-color);
border: 1px solid #999;
border-top: none;
z-index: 2000 !important;
@@ -594,7 +702,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 {
@@ -660,7 +769,7 @@ div[data-notify="container"] {
position: absolute;
right: 10px;
top: 11px;
font-size: x-large;
font-size: 150%;
color: #777;
z-index: 100;
}
@@ -721,4 +830,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;
}

View File

@@ -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;
@@ -49,7 +50,7 @@ async function downloadFile(req, res) {
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);

View File

@@ -7,6 +7,7 @@ const tarImportService = require('../../services/import/tar');
const singleImportService = require('../../services/import/single');
const cls = require('../../services/cls');
const path = require('path');
const noteCacheService = require('../../services/note_cache');
async function importToBranch(req) {
const parentNoteId = req.params.parentNoteId;
@@ -28,24 +29,32 @@ async function importToBranch(req) {
// and may produce unintended consequences
cls.disableEntityEvents();
let note; // typically root of the import - client can show it after finishing the import
if (extension === '.tar') {
return await tarImportService.importTar(file.buffer, parentNote);
note = await tarImportService.importTar(file.buffer, parentNote);
}
else if (extension === '.opml') {
return await opmlImportService.importOpml(file.buffer, parentNote);
note = await opmlImportService.importOpml(file.buffer, parentNote);
}
else if (extension === '.md') {
return await singleImportService.importMarkdown(file, parentNote);
note = await singleImportService.importMarkdown(file, parentNote);
}
else if (extension === '.html' || extension === '.htm') {
return await singleImportService.importHtml(file, parentNote);
note = await singleImportService.importHtml(file, parentNote);
}
else if (extension === '.enex') {
return await enexImportService.importEnex(file, parentNote);
note = await enexImportService.importEnex(file, parentNote);
}
else {
return [400, `Unrecognized extension ${extension}, must be .tar or .opml`];
}
// import has deactivated note events so note cache is not updated
// instead we force it to reload (can be async)
noteCacheService.load();
return note;
}
module.exports = {

View File

@@ -23,7 +23,8 @@ async function loginSync(req) {
const now = new Date();
if (Math.abs(timestamp.getTime() - now.getTime()) > 5000) {
// login token is valid for 5 minutes
if (Math.abs(timestamp.getTime() - now.getTime()) > 5 * 60 * 1000) {
return [400, { message: 'Auth request time is out of sync' }];
}

View File

@@ -6,7 +6,7 @@ const log = require('../../services/log');
// 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);

View File

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

View File

@@ -16,6 +16,9 @@ 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,

View File

@@ -19,7 +19,6 @@ async function anonymize() {
await db.run("UPDATE notes SET title = 'title', content = 'text'");
await db.run("UPDATE note_revisions SET title = 'title', content = 'text'");
await db.run("UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL");
await db.run("UPDATE images SET data = NULL");
await db.run(`UPDATE options SET value = 'anonymized' WHERE name IN
('documentSecret', 'encryptedDataKey', 'passwordVerificationHash',
'passwordVerificationSalt', 'passwordDerivedKeySalt')`);

View File

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

View File

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

View File

@@ -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.
@@ -234,6 +235,11 @@ function BackendScriptApi(startNote, currentNote, originEntity) {
* @returns {Promise<void>}
*/
this.refreshTree = () => messagingService.sendMessageToAllClients({ type: 'refresh-tree' });
/**
* @return {{syncVersion, appVersion, buildRevision, dbVersion, dataDirectory, buildDate}|*} - object representing basic info about running Trilium version
*/
this.getAppInfo = () => appInfo
}
module.exports = BackendScriptApi;

View File

@@ -1 +1 @@
module.exports = { buildDate:"2019-01-04T23:33:32+01:00", buildRevision: "5d74dcd2564ff1341550ade1250aa9d790abc056" };
module.exports = { buildDate:"2019-01-22T23:01:32+01:00", buildRevision: "2ac560c56e2d347fccc0ad51b8d62999408a7f74" };

View File

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

View File

@@ -101,6 +101,26 @@ async function fixEmptyRelationTargets(errorList) {
}
}
async function fixUndeletedBranches() {
const undeletedBranches = await sql.getRows(`
SELECT
branchId, noteId
FROM
branches
JOIN notes USING(noteId)
WHERE
notes.isDeleted = 1
AND branches.isDeleted = 0`);
for (const {branchId, noteId} of undeletedBranches) {
const branch = await repository.getBranch(branchId);
branch.isDeleted = true;
await branch.save();
log.info(`Branch ${branchId} has been deleted since associated note ${noteId} is deleted.`);
}
}
async function runAllChecks() {
const errorList = [];
@@ -125,16 +145,7 @@ async function runAllChecks() {
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 fixUndeletedBranches();
await runCheck(`
SELECT

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();
@@ -42,11 +42,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);

View File

@@ -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'
@@ -219,9 +220,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);

View File

@@ -37,7 +37,7 @@ eventService.subscribe(eventService.NOTE_TITLE_CHANGED, async note => {
}
});
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);
}

View File

@@ -1,6 +1,7 @@
"use strict";
const repository = require('./repository');
const log = require('./log');
const protectedSessionService = require('./protected_session');
const noteService = require('./notes');
const imagemin = require('imagemin');
@@ -13,7 +14,13 @@ const sanitizeFilename = require('sanitize-filename');
async function saveImage(buffer, originalName, parentNoteId) {
const resizedImage = await resize(buffer);
const optimizedImage = await optimize(resizedImage);
let optimizedImage;
try {
optimizedImage = await optimize(resizedImage);
} catch (e) {
log.error(e);
optimizedImage = resizedImage;
}
const imageFormat = imageType(optimizedImage);

View File

@@ -1,4 +1,5 @@
const sax = require("sax");
const fileType = require('file-type');
const stream = require('stream');
const xml2js = require('xml2js');
const log = require("../log");
@@ -144,7 +145,7 @@ async function importEnex(file, parentNote) {
});
}
else if (currentTag === 'mime') {
resource.mime = text;
resource.mime = text.toLowerCase();
if (text.startsWith("image/")) {
resource.title = "image";
@@ -222,7 +223,26 @@ async function importEnex(file, parentNote) {
const mediaRegex = new RegExp(`<en-media hash="${hash}"[^>]*>`, 'g');
if (resource.mime.startsWith("image/")) {
const fileTypeFromBuffer = fileType(resource.content);
if (fileTypeFromBuffer) {
// If fileType returns something for buffer, then set the mime given
resource.mime = fileTypeFromBuffer.mime;
}
const createResourceNote = async () => {
const resourceNote = (await noteService.createNote(noteEntity.noteId, resource.title, resource.content, {
attributes: resource.attributes,
type: 'file',
mime: resource.mime
})).note;
const resourceLink = `<a href="#root/${resourceNote.noteId}">${utils.escapeHtml(resource.title)}</a>`;
noteEntity.content = noteEntity.content.replace(mediaRegex, resourceLink);
}
if (["image/jpeg", "image/png", "image/gif"].includes(resource.mime)) {
try {
const originalName = "image." + resource.mime.substr(6);
const { url } = await imageService.saveImage(resource.content, originalName, noteEntity.noteId);
@@ -236,17 +256,13 @@ async function importEnex(file, parentNote) {
// otherwise image would be removed since no note would include it
note.content += imageLink;
}
} catch (e) {
log.error("error when saving image from ENEX file: " + e);
await createResourceNote();
}
}
else {
const resourceNote = (await noteService.createNote(noteEntity.noteId, resource.title, resource.content, {
attributes: resource.attributes,
type: 'file',
mime: resource.mime
})).note;
const resourceLink = `<a href="#root/${resourceNote.noteId}">${utils.escapeHtml(resource.title)}</a>`;
noteEntity.content = noteEntity.content.replace(mediaRegex, resourceLink);
await createResourceNote();
}
}

View File

@@ -33,9 +33,21 @@ async function load() {
archived = await sql.getMap(`SELECT noteId, isInheritable FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name = 'archived'`);
if (protectedSessionService.isProtectedSessionAvailable()) {
await loadProtectedNotes();
}
loaded = true;
}
async function loadProtectedNotes() {
protectedNoteTitles = await sql.getMap(`SELECT noteId, title FROM notes WHERE isDeleted = 0 AND isProtected = 1`);
for (const noteId in protectedNoteTitles) {
protectedNoteTitles[noteId] = protectedSessionService.decryptNoteTitle(noteId, protectedNoteTitles[noteId]);
}
}
function highlightResults(results, allTokens) {
// we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks
// which would make the resulting HTML string invalid.
@@ -314,7 +326,16 @@ eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED
delete childToParent[note.noteId];
}
else {
noteTitles[note.noteId] = note.title;
if (note.isProtected) {
// we can assume we have protected session since we managed to update
// removing from the maps is important when switching between protected & unprotected
protectedNoteTitles[note.noteId] = note.title;
delete noteTitles[note.noteId];
}
else {
noteTitles[note.noteId] = note.title;
delete protectedNoteTitles[note.noteId];
}
}
}
else if (entityName === 'branches') {
@@ -355,15 +376,9 @@ eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED
}
});
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, async () => {
if (!loaded) {
return;
}
protectedNoteTitles = await sql.getMap(`SELECT noteId, title FROM notes WHERE isDeleted = 0 AND isProtected = 1`);
for (const noteId in protectedNoteTitles) {
protectedNoteTitles[noteId] = protectedSessionService.decryptNoteTitle(noteId, protectedNoteTitles[noteId]);
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
if (loaded) {
loadProtectedNotes();
}
});
@@ -372,5 +387,6 @@ sqlInit.dbReady.then(() => utils.stopWatch("Autocomplete load", load));
module.exports = {
findNotes,
getNotePath,
getNoteTitleForPath
getNoteTitleForPath,
load
};

View File

@@ -1,4 +1,5 @@
const sql = require('./sql');
const sqlInit = require('./sql_init');
const optionService = require('./options');
const dateUtils = require('./date_utils');
const syncTableService = require('./sync_table');
@@ -358,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);
@@ -408,10 +399,12 @@ async function cleanupDeletedNotes() {
await sql.execute("UPDATE note_revisions SET content = NULL WHERE note_revisions.content IS NOT NULL AND noteId IN (SELECT noteId FROM notes WHERE isDeleted = 1 AND notes.dateModified <= ?)", [dateUtils.dateStr(cutoffDate)]);
}
// first cleanup kickoff 5 minutes after startup
setTimeout(cls.wrap(cleanupDeletedNotes), 5 * 60 * 1000);
sqlInit.dbReady.then(() => {
// first cleanup kickoff 5 minutes after startup
setTimeout(cls.wrap(cleanupDeletedNotes), 5 * 60 * 1000);
setInterval(cls.wrap(cleanupDeletedNotes), 4 * 3600 * 1000);
setInterval(cls.wrap(cleanupDeletedNotes), 4 * 3600 * 1000);
});
module.exports = {
createNewNote,

View File

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

View File

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

View File

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

View File

@@ -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]);
@@ -122,7 +117,6 @@ module.exports = {
getEntity,
getNote,
getBranch,
getImage,
getAttribute,
getOption,
updateEntity,

View File

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

View File

@@ -6,7 +6,7 @@ const sourceIdService = require('./source_id');
const log = require('./log');
async function executeNote(note, originEntity) {
if (!note.isJavaScript()) {
if (!note.isJavaScript() || note.getScriptEnv() !== 'backend' || !note.isContentAvailable) {
return;
}
@@ -79,7 +79,24 @@ 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;
}
if (!note.isJavaScript() && !note.isHtml()) {
return;
}
@@ -149,14 +166,8 @@ 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,
executeScript,
getScriptBundle,
getScriptBundleForNoteId
getScriptBundleForFrontend
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
<!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;">
<a id="history-back-button" title="Go to previous note." class="icon-action jam jam-arrow-square-left"></a>
&nbsp; &nbsp;
&nbsp;
<a id="history-forward-button" title="Go to next note." class="icon-action jam jam-arrow-square-right"></a>
</div>
@@ -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" 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>
&nbsp;

View File

@@ -1,7 +1,12 @@
<div id="note-detail-search" class="note-detail-component">
<div style="display: flex; align-items: center;">
<strong>Search string: &nbsp; &nbsp;</strong>
<textarea rows="4" cols="50" id="search-string"></textarea>
<textarea rows="4" cols="40" id="search-string"></textarea>
<span>
&nbsp; &nbsp;
<button type="button" class="btn btn-primary" id="note-detail-search-refresh-results-button">Refresh tree</button>
</span>
</div>
<br />

View File

@@ -14,7 +14,7 @@
<div id="confirm-dialog-custom"></div>
</div>
<div class="modal-footer">
<button class="btn btn-default btn-sm" id="confirm-dialog-cancel-button">Cancel</button>
<button class="btn btn-secondary btn-sm" id="confirm-dialog-cancel-button">Cancel</button>
&nbsp;

View File

@@ -55,6 +55,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">
@@ -108,7 +148,7 @@
<div style="display: flex; justify-content: space-between;">
<button class="btn btn-primary">Save</button>
<button class="btn btn-default" type="button" data-help-page="Protected-notes">Help</button>
<button class="btn btn-secondary" type="button" data-help-page="Protected-notes">Help</button>
</div>
</form>
</div>
@@ -125,7 +165,7 @@
<div style="display: flex; justify-content: space-between;">
<button class="btn btn-primary">Save</button>
<button class="btn btn-default" type="button" data-help-page="Note-revisions">Help</button>
<button class="btn btn-secondary" type="button" data-help-page="Note-revisions">Help</button>
</div>
</form>
</div>
@@ -154,7 +194,7 @@
<div style="display: flex; justify-content: space-between;">
<button class="btn btn-primary">Save</button>
<button class="btn btn-default" type="button" data-help-page="Synchronization">Help</button>
<button class="btn btn-secondary" type="button" data-help-page="Synchronization">Help</button>
</div>
</form>
@@ -164,24 +204,24 @@
<p>This will test connection and handshake to the sync server. If sync server isn't initialized, this will set it up to sync with local document.</p>
<button id="test-sync-button" class="btn btn-default">Test sync</button>
<button id="test-sync-button" class="btn btn-secondary">Test sync</button>
</div>
<div id="advanced" class="tab-pane">
<h4 style="margin-top: 0px;">Sync</h4>
<button id="force-full-sync-button" class="btn btn-default">Force full sync</button>
<button id="force-full-sync-button" class="btn btn-secondary">Force full sync</button>
<br/>
<br/>
<button id="fill-sync-rows-button" class="btn btn-default">Fill sync rows</button>
<button id="fill-sync-rows-button" class="btn btn-secondary">Fill sync rows</button>
<br/>
<br/>
<h4>Debugging</h4>
<button id="anonymize-button" class="btn btn-default">Save anonymized database</button><br/><br/>
<button id="anonymize-button" class="btn btn-secondary">Save anonymized database</button><br/><br/>
<p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and metadata)
for sharing online for debugging purposes without fear of leaking your personal data.</p>
@@ -190,7 +230,7 @@
<p>This will rebuild database which will typically result in smaller database file. No data will be actually changed.</p>
<button id="vacuum-database-button" class="btn btn-default">Vacuum database</button>
<button id="vacuum-database-button" class="btn btn-secondary">Vacuum database</button>
</div>
<div id="about" class="tab-pane">

View File

@@ -7,7 +7,7 @@
</head>
<body>
<div class="container">
<div class="col-md-5 offset-md-3" style="padding-top: 25px;">
<div class="col-xs-12 col-sm-10 col-md-6 col-lg-4 col-xl-4 mx-auto" style="padding-top: 25px;">
<h1>Trilium login</h1>
<% if (failedAuth) { %>
@@ -60,6 +60,8 @@
device = /Mobi/.test(navigator.userAgent) ? "mobile" : "desktop";
}
console.log("Setting device cookie to:", device);
setCookie("trilium-device", device);
function setCookie(name, value) {

View File

@@ -2,113 +2,116 @@
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Setup</title>
</head>
<body>
<div id="setup-dialog" style="width: 700px; margin: auto; padding-top: 50px; display:none; font-size: larger;">
<h1>Trilium Notes setup</h1>
<div class="container">
<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;">
</div>
<div id="setup-type" data-bind="visible: step() == 'setup-type'" style="margin-top: 20px;">
<div class="radio" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="new-document" data-bind="checked: setupType">
I'm a new user and I want to create new Trilium document for my notes</label>
</div>
<div class="radio" data-bind="if: instanceType == 'server'" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupType">
I have desktop instance already and I want to setup sync with it</label>
</div>
<div class="radio" data-bind="if: instanceType == 'desktop'" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupType">
I have server instance up and I want to setup sync with it</label>
<div class="alert alert-warning" id="alert" style="display: none;">
</div>
<button type="button" data-bind="disable: !setupTypeSelected(), click: selectSetupType" class="btn btn-primary">Next</button>
</div>
<div id="setup-type" data-bind="visible: step() == 'setup-type'" style="margin-top: 20px;">
<div class="radio" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="new-document" data-bind="checked: setupType">
I'm a new user and I want to create new Trilium document for my notes</label>
</div>
<div class="radio" data-bind="if: instanceType == 'server'" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupType">
I have desktop instance already and I want to setup sync with it</label>
</div>
<div class="radio" data-bind="if: instanceType == 'desktop'" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupType">
I have server instance up and I want to setup sync with it</label>
</div>
<div data-bind="visible: step() == 'new-document'">
<h2>New document</h2>
<p>You're almost done with the setup. The last thing is to choose username and password using which you'll login to the application.
This password is also used for generating encryption key which encrypts protected notes.</p>
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" data-bind="value: username" placeholder="Choose alphanumeric username">
</div>
<div class="form-group">
<label for="password1">Password</label>
<input type="password" class="form-control" data-bind="value: password1" placeholder="Password">
</div>
<div class="form-group">
<label for="password2">Repeat password</label>
<input type="password" class="form-control" data-bind="value: password2" placeholder="Password">
<button type="button" data-bind="disable: !setupTypeSelected(), click: selectSetupType" class="btn btn-primary">Next</button>
</div>
<button type="button" data-bind="click: back" class="btn btn-default">Back</button>
<div data-bind="visible: step() == 'new-document'">
<h2>New document</h2>
&nbsp;
<p>You're almost done with the setup. The last thing is to choose username and password using which you'll login to the application.
This password is also used for generating encryption key which encrypts protected notes.</p>
<button type="button" data-bind="click: finish" class="btn btn-primary">Finish setup</button>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" data-bind="value: username" placeholder="Choose alphanumeric username">
</div>
<div class="form-group">
<label for="password1">Password</label>
<input type="password" class="form-control" data-bind="value: password1" placeholder="Password">
</div>
<div class="form-group">
<label for="password2">Repeat password</label>
<input type="password" class="form-control" data-bind="value: password2" placeholder="Password">
</div>
<div data-bind="visible: step() == 'sync-from-desktop'">
<h2>Sync from Desktop</h2>
<button type="button" data-bind="click: back" class="btn btn-secondary">Back</button>
<p>This setup needs to be initiated from the desktop instance:</p>
&nbsp;
<ol>
<li>please open your desktop instance of Trilium Notes</li>
<li>click on Options button in the top right</li>
<li>click on Sync tab</li>
<li>configure server instance address to the: <span id="current-host"></span> and click save.</li>
<li>click on "Test sync" button</li>
<li>once you've done all this, click <a href="/">here</a></li>
</ol>
<button type="button" data-bind="click: back" class="btn btn-default">Back</button>
</div>
<div data-bind="visible: step() == 'sync-from-server'">
<h2>Sync from Server</h2>
<p>Please enter Trilium server address and credentials below. This will download the whole Trilium document from server and setup sync to it. Depending on the document size and your connection speed, this may take a while.</p>
<div class="form-group">
<label for="sync-server-host">Trilium server address</label>
<input type="text" id="syncServerHost" class="form-control" data-bind="value: syncServerHost" placeholder="https://<hostname>:<port>">
</div>
<div class="form-group">
<label for="sync-proxy">Proxy server (optional)</label>
<input type="text" id="sync-proxy" class="form-control" data-bind="value: syncProxy" placeholder="https://<hostname>:<port>">
<p><strong>Note:</strong> If you leave proxy setting blank, system proxy will be used (applies to desktop/electron build only)</p>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" class="form-control" data-bind="value: username" placeholder="Username">
</div>
<div class="form-group">
<label for="password1">Password</label>
<input type="password" id="password1" class="form-control" data-bind="value: password1" placeholder="Password">
<button type="button" data-bind="click: finish" class="btn btn-primary">Finish setup</button>
</div>
<button type="button" data-bind="click: back" class="btn btn-default">Back</button>
<div data-bind="visible: step() == 'sync-from-desktop'">
<h2>Sync from Desktop</h2>
&nbsp;
<p>This setup needs to be initiated from the desktop instance:</p>
<button type="button" data-bind="click: finish" class="btn btn-primary">Finish setup</button>
</div>
<ol>
<li>please open your desktop instance of Trilium Notes</li>
<li>click on Options button in the top right</li>
<li>click on Sync tab</li>
<li>configure server instance address to the: <span id="current-host"></span> and click save.</li>
<li>click on "Test sync" button</li>
<li>once you've done all this, click <a href="/">here</a></li>
</ol>
<div data-bind="visible: step() == 'sync-in-progress'">
<h2>Sync in progress</h2>
<button type="button" data-bind="click: back" class="btn btn-secondary">Back</button>
</div>
<div class="alert alert-success">Sync has been correctly set up. It will take some time for the initial sync to finish. Once it's done, you'll be redirected to the login page.</div>
<div data-bind="visible: step() == 'sync-from-server'">
<h2>Sync from Server</h2>
<div data-bind="if: instanceType == 'desktop'">
Outstanding sync items: <strong id="outstanding-syncs">N/A</strong>
<p>Please enter Trilium server address and credentials below. This will download the whole Trilium document from server and setup sync to it. Depending on the document size and your connection speed, this may take a while.</p>
<div class="form-group">
<label for="sync-server-host">Trilium server address</label>
<input type="text" id="syncServerHost" class="form-control" data-bind="value: syncServerHost" placeholder="https://<hostname>:<port>">
</div>
<div class="form-group">
<label for="sync-proxy">Proxy server (optional)</label>
<input type="text" id="sync-proxy" class="form-control" data-bind="value: syncProxy" placeholder="https://<hostname>:<port>">
<p><strong>Note:</strong> If you leave proxy setting blank, system proxy will be used (applies to desktop/electron build only)</p>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" class="form-control" data-bind="value: username" placeholder="Username">
</div>
<div class="form-group">
<label for="password1">Password</label>
<input type="password" id="password1" class="form-control" data-bind="value: password1" placeholder="Password">
</div>
<button type="button" data-bind="click: back" class="btn btn-secondary">Back</button>
&nbsp;
<button type="button" data-bind="click: finish" class="btn btn-primary">Finish setup</button>
</div>
<div data-bind="visible: step() == 'sync-in-progress'">
<h2>Sync in progress</h2>
<div class="alert alert-success">Sync has been correctly set up. It will take some time for the initial sync to finish. Once it's done, you'll be redirected to the login page.</div>
<div data-bind="if: instanceType == 'desktop'">
Outstanding sync items: <strong id="outstanding-syncs">N/A</strong>
</div>
</div>
</div>
</div>
@@ -125,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>