mirror of
https://github.com/zadam/trilium.git
synced 2025-10-27 08:16:40 +01:00
Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
780f462e94 | ||
|
|
488e657cc4 | ||
|
|
8bc2a21d80 | ||
|
|
743d72a0c3 | ||
|
|
20b1357be6 | ||
|
|
d9f2bb37e7 | ||
|
|
97c1b3061f | ||
|
|
c022fcf196 | ||
|
|
b5baab056c | ||
|
|
edc9a1a2bf | ||
|
|
c0e45a73a8 | ||
|
|
784cd62df1 | ||
|
|
91cf090820 | ||
|
|
d9f29cbf27 | ||
|
|
23a5e38e02 | ||
|
|
663bd1a8fe | ||
|
|
a6a687c4a6 | ||
|
|
f2aaf8b0a3 | ||
|
|
01ede22504 | ||
|
|
b6d617aefa | ||
|
|
7921850186 | ||
|
|
244a4562b1 | ||
|
|
07c33979c3 | ||
|
|
353a9b24c1 | ||
|
|
548ecd4171 | ||
|
|
8d9b0db316 | ||
|
|
96a44a9a0c | ||
|
|
b545100cad | ||
|
|
e32289720c | ||
|
|
550bb77ca9 | ||
|
|
664a87cdd5 | ||
|
|
53ee1fa5ed | ||
|
|
361d8a4216 | ||
|
|
ae6e222c50 | ||
|
|
37995f1ce5 | ||
|
|
ad7fa5e096 | ||
|
|
3585982758 | ||
|
|
c776f298f2 | ||
|
|
f07c427da1 | ||
|
|
e560072f8b | ||
|
|
3f976a3821 | ||
|
|
274bb32696 | ||
|
|
99b163a042 | ||
|
|
fdcc833f6d | ||
|
|
a8e45019e4 | ||
|
|
7f7028873c | ||
|
|
2d2d76a715 | ||
|
|
69cbfaae17 | ||
|
|
aebce8f12b | ||
|
|
045ca1f0bf | ||
|
|
bf2db6eac7 | ||
|
|
cf84114f91 | ||
|
|
6426157bb3 | ||
|
|
332fc16852 | ||
|
|
da2cd57428 | ||
|
|
de9bab1181 | ||
|
|
136375cf11 | ||
|
|
eabc7f80b7 | ||
|
|
6405d6e066 | ||
|
|
f6d481a9e2 | ||
|
|
695f0e5879 | ||
|
|
ae337e4500 | ||
|
|
19ffa14f10 | ||
|
|
bf3f26fde8 | ||
|
|
dece400207 | ||
|
|
baab745462 | ||
|
|
0d3b3ec7c5 | ||
|
|
7aff20bb0d | ||
|
|
5acf84aece | ||
|
|
c58a0df76d | ||
|
|
20c14a1920 | ||
|
|
04063d8a9c | ||
|
|
dd69e0135b | ||
|
|
ab6e78f726 | ||
|
|
9029d18178 | ||
|
|
e9a1791e3d | ||
|
|
70e13c8a20 | ||
|
|
7e1cc729f9 | ||
|
|
87e7828440 | ||
|
|
29fd78aee5 | ||
|
|
f90c2317fc | ||
|
|
bca5087426 | ||
|
|
583123ab0a | ||
|
|
12f70b28c8 | ||
|
|
31a4a201a8 | ||
|
|
5ec866aa50 | ||
|
|
cdede3240c |
29
README.md
29
README.md
@@ -1,12 +1,12 @@
|
|||||||
# Trilium
|
# Trilium Notes
|
||||||
Hierarchical note taking application.
|
Trilium Notes is a hierarchical note taking application. Picture tells a thousand words:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Notes can be arranged into arbitrarily deep hierarchy
|
* Notes can be arranged into arbitrarily deep hierarchy
|
||||||
* Notes can have more than 1 parents - see [cloning](https://github.com/zadam/trilium/wiki/Cloning)
|
* Notes can have more than 1 parents - see [cloning](https://github.com/zadam/trilium/wiki/Cloning-notes)
|
||||||
* WYSIWYG (What You See Is What You Get) editing
|
* WYSIWYG (What You See Is What You Get) editing
|
||||||
* Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation)
|
* Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation)
|
||||||
* Seamless note versioning
|
* Seamless note versioning
|
||||||
@@ -16,17 +16,28 @@ Hierarchical note taking application.
|
|||||||
|
|
||||||
## Builds
|
## Builds
|
||||||
|
|
||||||
* If you want to install Trilium on server, follow [this page](https://github.com/zadam/trilium/wiki/Installation-on-server)
|
* If you want to install Trilium on server, follow [this page](https://github.com/zadam/trilium/wiki/Installation-as-webapp)
|
||||||
* If you want to use Trilium on the desktop, download binary release from [releases], unzip the package and run ```trilium``` executable.
|
* If you want to use Trilium on the desktop, download binary release for your platfor from [latest release](https://github.com/zadam/trilium/releases/latest), unzip the package and run ```trilium``` executable.
|
||||||
|
|
||||||
## Supported platforms
|
## Supported platforms
|
||||||
|
|
||||||
Desktop (electron) builds are available for Linux and Windows.
|
Desktop (electron-based) 64-bit builds are available for Linux and Windows.
|
||||||
|
|
||||||
Requirements for web based installation are outlined in (https://github.com/zadam/trilium/wiki/Installation-on-server).
|
Requirements for web based installation are [outlined here](https://github.com/zadam/trilium/wiki/Installation-as-webapp).
|
||||||
|
|
||||||
Currently only recent Chrome and Firefox are supported (tested) browsers. Other modern browsers should work as well.
|
Currently only recent Chrome and Firefox are supported (tested) browsers. Other modern browsers (not IE) might work as well.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
See [wiki](https://github.com/zadam/trilium/wiki/Home) for complete list of available pages.
|
List of documentation pages:
|
||||||
|
|
||||||
|
* [Installation as webapp](https://github.com/zadam/trilium/wiki/Installation-as-webapp)
|
||||||
|
* [Note navigation](https://github.com/zadam/trilium/wiki/Note-navigation)
|
||||||
|
* [Tree manipulation](https://github.com/zadam/trilium/wiki/Tree-manipulation)
|
||||||
|
* [Links](https://github.com/zadam/trilium/wiki/Links)
|
||||||
|
* [Cloning notes](https://github.com/zadam/trilium/wiki/Cloning-notes)
|
||||||
|
* [Protected notes](https://github.com/zadam/trilium/wiki/Protected-notes)
|
||||||
|
* [Synchronization](https://github.com/zadam/trilium/wiki/Synchronization)
|
||||||
|
* [Document](https://github.com/zadam/trilium/wiki/Document)
|
||||||
|
* [Keyboard shortcuts](https://github.com/zadam/trilium/wiki/Keyboard-shortcuts)
|
||||||
|
* [Troubleshooting](https://github.com/zadam/trilium/wiki/Troubleshooting)
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
./set-build.sh
|
|
||||||
|
|
||||||
echo "Deleting dist"
|
echo "Deleting dist"
|
||||||
|
|
||||||
@@ -10,15 +8,15 @@ cp -r ../trilium-node-binaries/sqlite/* node_modules/sqlite3/lib/binding/
|
|||||||
|
|
||||||
cp -r ../trilium-node-binaries/scrypt/* node_modules/scrypt/bin/
|
cp -r ../trilium-node-binaries/scrypt/* node_modules/scrypt/bin/
|
||||||
|
|
||||||
./node_modules/.bin/electron-rebuild
|
./node_modules/.bin/electron-rebuild --arch=ia32
|
||||||
|
|
||||||
./node_modules/.bin/electron-packager . --out=dist --platform=linux,win32 --overwrite
|
./node_modules/.bin/electron-packager . --out=dist --platform=linux --arch=ia32 --overwrite
|
||||||
|
|
||||||
|
./node_modules/.bin/electron-rebuild --arch=x64
|
||||||
|
|
||||||
|
./node_modules/.bin/electron-packager . --out=dist --platform=linux --arch=x64 --overwrite
|
||||||
|
|
||||||
|
./node_modules/.bin/electron-packager . --out=dist --platform=win32 --arch=x64 --overwrite
|
||||||
|
|
||||||
# can't copy this before the packaging because the same file name is used for both linux and windows build
|
# can't copy this before the packaging because the same file name is used for both linux and windows build
|
||||||
cp ../trilium-node-binaries/scrypt.node ./dist/trilium-win32-x64/resources/app/node_modules/scrypt/build/Release/
|
cp ../trilium-node-binaries/scrypt.node ./dist/trilium-win32-x64/resources/app/node_modules/scrypt/build/Release/
|
||||||
|
|
||||||
echo "Packaging windows distribution..."
|
|
||||||
|
|
||||||
# possibly use zip: zip -r myfiles.zip mydir
|
|
||||||
|
|
||||||
tar cfJ dist/win.tar.xz dist/trilium-win32-x64
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
# Script generates certificate by default into the ~/trilium-data/cert where it is expected by Trilium
|
# Script generates certificate by default into the ~/trilium-data/cert where it is expected by Trilium
|
||||||
# If directory is given in argument, certificate will be created there.
|
# If directory is given in argument, certificate will be created there.
|
||||||
|
|
||||||
14
bin/package.sh
Executable file
14
bin/package.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
VERSION=`jq -r ".version" package.json`
|
||||||
|
|
||||||
|
cd dist
|
||||||
|
|
||||||
|
echo "Packaging linux x64 electron distribution..."
|
||||||
|
7z a trilium-linux-x64-${VERSION}.7z trilium-linux-x64
|
||||||
|
|
||||||
|
echo "Packaging linux ia32 electron distribution..."
|
||||||
|
7z a trilium-linux-ia32-${VERSION}.7z trilium-linux-ia32
|
||||||
|
|
||||||
|
echo "Packaging windows x64 electron distribution..."
|
||||||
|
7z a trilium-windows-x64-${VERSION}.7z trilium-win32-x64
|
||||||
78
bin/release.sh
Executable file
78
bin/release.sh
Executable file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [[ $# -eq 0 ]] ; then
|
||||||
|
echo "Missing argument of new version"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION=$1
|
||||||
|
|
||||||
|
if ! [[ ${VERSION} =~ ^[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}(-.+)?$ ]] ;
|
||||||
|
then
|
||||||
|
echo "Version ${VERSION} isn't in format X.Y.Z"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git diff-index --quiet HEAD --; then
|
||||||
|
echo "There are uncommitted changes"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Releasing Trilium $VERSION"
|
||||||
|
|
||||||
|
jq '.version = "'$VERSION'"' package.json|sponge package.json
|
||||||
|
|
||||||
|
git add package.json
|
||||||
|
|
||||||
|
echo 'module.exports = { build_date:"'`date --iso-8601=seconds`'", build_revision: "'`git log -1 --format="%H"`'" };' > services/build.js
|
||||||
|
|
||||||
|
git add services/build.js
|
||||||
|
|
||||||
|
TAG=v$VERSION
|
||||||
|
|
||||||
|
echo "Committing package.json version change"
|
||||||
|
|
||||||
|
git commit -m "release $VERSION"
|
||||||
|
git push
|
||||||
|
|
||||||
|
echo "Tagging commit with $TAG"
|
||||||
|
|
||||||
|
git tag $TAG
|
||||||
|
git push origin $TAG
|
||||||
|
|
||||||
|
bin/build.sh
|
||||||
|
|
||||||
|
bin/package.sh
|
||||||
|
|
||||||
|
LINUX_X64_BUILD=trilium-linux-x64-$VERSION.7z
|
||||||
|
LINUX_IA32_BUILD=trilium-linux-ia32-$VERSION.7z
|
||||||
|
WINDOWS_X64_BUILD=trilium-windows-x64-$VERSION.7z
|
||||||
|
|
||||||
|
echo "Creating release in GitHub"
|
||||||
|
|
||||||
|
github-release release \
|
||||||
|
--tag $TAG \
|
||||||
|
--name "$TAG release"
|
||||||
|
|
||||||
|
echo "Uploading linux x64 build"
|
||||||
|
|
||||||
|
github-release upload \
|
||||||
|
--tag $TAG \
|
||||||
|
--name "$LINUX_X64_BUILD" \
|
||||||
|
--file "dist/$LINUX_X64_BUILD"
|
||||||
|
|
||||||
|
echo "Uploading linux ia32 build"
|
||||||
|
|
||||||
|
github-release upload \
|
||||||
|
--tag $TAG \
|
||||||
|
--name "$LINUX_IA32_BUILD" \
|
||||||
|
--file "dist/$LINUX_IA32_BUILD"
|
||||||
|
|
||||||
|
echo "Uploading windows x64 build"
|
||||||
|
|
||||||
|
github-release upload \
|
||||||
|
--tag $TAG \
|
||||||
|
--name "$WINDOWS_X64_BUILD" \
|
||||||
|
--file "dist/$WINDOWS_X64_BUILD"
|
||||||
|
|
||||||
|
echo "Release finished!"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('root', 'root', 'root', 0, 0, '2017-12-22T11:41:07.000Z', '2017-12-22T11:41:07.000Z');
|
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('root', 'root', 'root', 0, 0, '2017-12-22T11:41:07.000Z', '2017-12-22T11:41:07.000Z');
|
||||||
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('1Heh2acXfPNt', 'Trilium Demo', '<p>Welcome to Trilium!</p><p> </p><p>This is initial document provided by default Trilium to showcase some of its features and also give you some ideas how you might structure your notes. You can play with it, modify note content and tree structure as you wish.</p><p> </p><p>If you need any help, visit Trilium wesite: <a href="https://github.com/zadam/trilium">https://github.com/zadam/trilium</a></p><p> </p><p>Once you''re finished with experimenting and want to cleanup these pages, you can simply delete them all.</p>', 0, 0, '2017-12-23T00:46:39.304Z', '2017-12-23T04:08:45.445Z');
|
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('1Heh2acXfPNt', 'Trilium Demo', '<p><strong>Welcome to Trilium Notes!</strong></p><p> </p><p>This is initial document provided by default Trilium to showcase some of its features and also give you some ideas how you might structure your notes. You can play with it, modify note content and tree structure as you wish.</p><p> </p><p>If you need any help, visit Trilium wesite: <a href="https://github.com/zadam/trilium">https://github.com/zadam/trilium</a></p><h3>Cleanup</h3><p>Once you''re finished with experimenting and want to cleanup these pages, you can simply delete them all.</p><h3>Formatting</h3><p>Trilium supports classic formatting like <i>italic</i>, <strong>bold</strong>, <i><strong>bold and italic</strong></i>. Of course you can add links like this one pointing to <a href="http://www.google.com">google.com</a></p><h4>Lists</h4><p><strong>Ordered:</strong></p><ol><li>First Item</li><li>Second item<ol><li>First sub-item</li><li>Second sub-item</li></ol></li></ol><p> </p><p><strong>Unordered:</strong></p><ul><li>Item</li><li>Another item<ul><li>Sub-item<ul><li>Sub-sub-item</li></ul></li></ul></li></ul><h4>Block quotes</h4><blockquote><p>Whereof one cannot speak, thereof one must be silent”</p><p>– Ludwig Wittgenstein</p></blockquote><p> </p>', 0, 0, '2017-12-23T00:46:39.304Z', '2017-12-23T04:08:45.445Z');
|
||||||
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('3RkyK9LI18dO', 'Journal', '<p>Expand note on the left pane to see content.</p>', 0, 0, '2017-12-23T01:20:04.181Z', '2017-12-23T18:07:55.377Z');
|
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('3RkyK9LI18dO', 'Journal', '<p>Expand note on the left pane to see content.</p>', 0, 0, '2017-12-23T01:20:04.181Z', '2017-12-23T18:07:55.377Z');
|
||||||
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('L1Ox40M1aEyy', '2016', '<p>No content.</p><p> </p><p> </p><p> </p><p> </p>', 0, 0, '2017-12-23T01:20:45.365Z', '2017-12-23T16:40:43.129Z');
|
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('L1Ox40M1aEyy', '2016', '<p>No content.</p><p> </p><p> </p><p> </p><p> </p>', 0, 0, '2017-12-23T01:20:45.365Z', '2017-12-23T16:40:43.129Z');
|
||||||
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('HJusZTbBU494', '2017', '<p>No content.</p>', 0, 0, '2017-12-23T01:20:50.709Z', '2017-12-23T16:41:03.119Z');
|
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('HJusZTbBU494', '2017', '<p>No content.</p>', 0, 0, '2017-12-23T01:20:50.709Z', '2017-12-23T16:41:03.119Z');
|
||||||
|
|||||||
@@ -67,9 +67,6 @@ CREATE INDEX `IDX_sync_sync_date` ON `sync` (
|
|||||||
CREATE INDEX `IDX_notes_is_deleted` ON `notes` (
|
CREATE INDEX `IDX_notes_is_deleted` ON `notes` (
|
||||||
`is_deleted`
|
`is_deleted`
|
||||||
);
|
);
|
||||||
CREATE INDEX `IDX_notes_tree_note_tree_id` ON `notes_tree` (
|
|
||||||
`note_tree_id`
|
|
||||||
);
|
|
||||||
CREATE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
|
CREATE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
|
||||||
`note_id`,
|
`note_id`,
|
||||||
`parent_note_id`
|
`parent_note_id`
|
||||||
|
|||||||
1
index.js
1
index.js
@@ -21,6 +21,7 @@ function createMainWindow() {
|
|||||||
const win = new electron.BrowserWindow({
|
const win = new electron.BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 900,
|
height: 900,
|
||||||
|
title: 'Trilium Notes',
|
||||||
icon: path.join(__dirname, 'public/images/app-icons/png/256x256.png')
|
icon: path.join(__dirname, 'public/images/app-icons/png/256x256.png')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
6
migrations/0061__change_index_to_unique.sql
Normal file
6
migrations/0061__change_index_to_unique.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
DROP INDEX IDX_notes_tree_note_id_parent_note_id;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
|
||||||
|
`note_id`,
|
||||||
|
`parent_note_id`
|
||||||
|
);
|
||||||
9
migrations/0062__change_index_back_to_non_unique.sql
Normal file
9
migrations/0062__change_index_back_to_non_unique.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
DROP INDEX IDX_notes_tree_note_id_parent_note_id;
|
||||||
|
|
||||||
|
CREATE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
|
||||||
|
`note_id`,
|
||||||
|
`parent_note_id`
|
||||||
|
);
|
||||||
|
|
||||||
|
-- dropping this as it's just duplicate of primary key
|
||||||
|
DROP INDEX IDX_notes_tree_note_tree_id;
|
||||||
11
migrations/0063__image_table.sql
Normal file
11
migrations/0063__image_table.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE images
|
||||||
|
(
|
||||||
|
image_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
format TEXT NOT NULL,
|
||||||
|
checksum TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
data BLOB,
|
||||||
|
is_deleted INT NOT NULL DEFAULT 0,
|
||||||
|
date_modified TEXT NOT NULL,
|
||||||
|
date_created TEXT NOT NULL
|
||||||
|
);
|
||||||
16
migrations/0064__add_note_id_to_image_table.sql
Normal file
16
migrations/0064__add_note_id_to_image_table.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
DROP TABLE images;
|
||||||
|
|
||||||
|
CREATE TABLE images
|
||||||
|
(
|
||||||
|
image_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
note_id TEXT NOT NULL,
|
||||||
|
format TEXT NOT NULL,
|
||||||
|
checksum TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
data BLOB,
|
||||||
|
is_deleted INT NOT NULL DEFAULT 0,
|
||||||
|
date_modified TEXT NOT NULL,
|
||||||
|
date_created TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX images_note_id_index ON images (note_id);
|
||||||
27
migrations/0065__notes_image.sql
Normal file
27
migrations/0065__notes_image.sql
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
DROP TABLE images;
|
||||||
|
|
||||||
|
CREATE TABLE images
|
||||||
|
(
|
||||||
|
image_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
format TEXT NOT NULL,
|
||||||
|
checksum TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
data BLOB,
|
||||||
|
is_deleted INT NOT NULL DEFAULT 0,
|
||||||
|
date_modified TEXT NOT NULL,
|
||||||
|
date_created TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE notes_image
|
||||||
|
(
|
||||||
|
note_image_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
note_id TEXT NOT NULL,
|
||||||
|
image_id TEXT NOT NULL,
|
||||||
|
is_deleted INT NOT NULL DEFAULT 0,
|
||||||
|
date_modified TEXT NOT NULL,
|
||||||
|
date_created TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX notes_image_note_id_index ON notes_image (note_id);
|
||||||
|
CREATE INDEX notes_image_image_id_index ON notes_image (image_id);
|
||||||
|
CREATE INDEX notes_image_note_id_image_id_index ON notes_image (note_id, image_id);
|
||||||
2318
package-lock.json
generated
2318
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -1,7 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "trilium",
|
"name": "trilium",
|
||||||
"description": "Trilium",
|
"description": "Trilium Notes",
|
||||||
"version": "0.0.9",
|
"version": "0.3.0",
|
||||||
|
"license": "AGPL-3.0-only",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/zadam/trilium.git"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./bin/www",
|
"start": "node ./bin/www",
|
||||||
"test-electron": "xo",
|
"test-electron": "xo",
|
||||||
@@ -14,6 +19,7 @@
|
|||||||
"publish-forge": "electron-forge publish"
|
"publish-forge": "electron-forge publish"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"async-mutex": "^0.1.3",
|
||||||
"body-parser": "~1.18.2",
|
"body-parser": "~1.18.2",
|
||||||
"cookie-parser": "~1.4.3",
|
"cookie-parser": "~1.4.3",
|
||||||
"debug": "~3.1.0",
|
"debug": "~3.1.0",
|
||||||
@@ -23,15 +29,24 @@
|
|||||||
"electron-debug": "^1.0.0",
|
"electron-debug": "^1.0.0",
|
||||||
"electron-in-page-search": "^1.2.4",
|
"electron-in-page-search": "^1.2.4",
|
||||||
"express": "~4.16.2",
|
"express": "~4.16.2",
|
||||||
|
"express-promise-wrap": "^0.2.2",
|
||||||
"express-session": "^1.15.6",
|
"express-session": "^1.15.6",
|
||||||
"fs-extra": "^4.0.2",
|
"fs-extra": "^4.0.2",
|
||||||
"helmet": "^3.9.0",
|
"helmet": "^3.9.0",
|
||||||
"html": "^1.0.0",
|
"html": "^1.0.0",
|
||||||
|
"image-type": "^3.0.0",
|
||||||
|
"imagemin": "^5.3.1",
|
||||||
|
"imagemin-giflossy": "^5.1.10",
|
||||||
|
"imagemin-mozjpeg": "^7.0.0",
|
||||||
|
"imagemin-pngquant": "^5.0.1",
|
||||||
"ini": "^1.3.4",
|
"ini": "^1.3.4",
|
||||||
|
"jimp": "^0.2.28",
|
||||||
|
"multer": "^1.3.0",
|
||||||
"rand-token": "^0.4.0",
|
"rand-token": "^0.4.0",
|
||||||
"request": "^2.83.0",
|
"request": "^2.83.0",
|
||||||
"request-promise": "^4.2.2",
|
"request-promise": "^4.2.2",
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
|
"sanitize-filename": "^1.6.1",
|
||||||
"scrypt": "^6.0.3",
|
"scrypt": "^6.0.3",
|
||||||
"serve-favicon": "~2.4.5",
|
"serve-favicon": "~2.4.5",
|
||||||
"session-file-store": "^1.1.2",
|
"session-file-store": "^1.1.2",
|
||||||
|
|||||||
@@ -3,54 +3,72 @@
|
|||||||
const contextMenu = (function() {
|
const contextMenu = (function() {
|
||||||
const treeEl = $("#tree");
|
const treeEl = $("#tree");
|
||||||
|
|
||||||
let clipboardId = null;
|
let clipboardIds = [];
|
||||||
let clipboardMode = null;
|
let clipboardMode = null;
|
||||||
|
|
||||||
function pasteAfter(node) {
|
function pasteAfter(node) {
|
||||||
if (clipboardMode === 'cut') {
|
if (clipboardMode === 'cut') {
|
||||||
const subjectNode = treeUtils.getNodeByKey(clipboardId);
|
for (const nodeKey of clipboardIds) {
|
||||||
|
const subjectNode = treeUtils.getNodeByKey(nodeKey);
|
||||||
|
|
||||||
treeChanges.moveAfterNode(subjectNode, node);
|
treeChanges.moveAfterNode([subjectNode], node);
|
||||||
|
}
|
||||||
|
|
||||||
|
clipboardIds = [];
|
||||||
|
clipboardMode = null;
|
||||||
}
|
}
|
||||||
else if (clipboardMode === 'copy') {
|
else if (clipboardMode === 'copy') {
|
||||||
treeChanges.cloneNoteAfter(clipboardId, node.data.note_tree_id);
|
for (const noteId of clipboardIds) {
|
||||||
|
treeChanges.cloneNoteAfter(noteId, node.data.note_tree_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
|
||||||
}
|
}
|
||||||
else if (clipboardId === null) {
|
else if (clipboardIds.length === 0) {
|
||||||
// just do nothing
|
// just do nothing
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throwError("Unrecognized clipboard mode=" + clipboardMode);
|
throwError("Unrecognized clipboard mode=" + clipboardMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
clipboardId = null;
|
|
||||||
clipboardMode = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pasteInto(node) {
|
function pasteInto(node) {
|
||||||
if (clipboardMode === 'cut') {
|
if (clipboardMode === 'cut') {
|
||||||
const subjectNode = treeUtils.getNodeByKey(clipboardId);
|
for (const nodeKey of clipboardIds) {
|
||||||
|
const subjectNode = treeUtils.getNodeByKey(nodeKey);
|
||||||
|
|
||||||
treeChanges.moveToNode(subjectNode, node);
|
treeChanges.moveToNode([subjectNode], node);
|
||||||
|
}
|
||||||
|
|
||||||
|
clipboardIds = [];
|
||||||
|
clipboardMode = null;
|
||||||
}
|
}
|
||||||
else if (clipboardMode === 'copy') {
|
else if (clipboardMode === 'copy') {
|
||||||
treeChanges.cloneNoteTo(clipboardId, node.data.note_id);
|
for (const noteId of clipboardIds) {
|
||||||
|
treeChanges.cloneNoteTo(noteId, node.data.note_id);
|
||||||
|
}
|
||||||
|
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
|
||||||
|
}
|
||||||
|
else if (clipboardIds.length === 0) {
|
||||||
|
// just do nothing
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throwError("Unrecognized clipboard mode=" + mode);
|
throwError("Unrecognized clipboard mode=" + mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
clipboardId = null;
|
|
||||||
clipboardMode = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function copy(node) {
|
function copy(nodes) {
|
||||||
clipboardId = node.data.note_id;
|
clipboardIds = nodes.map(node => node.data.note_id);
|
||||||
clipboardMode = 'copy';
|
clipboardMode = 'copy';
|
||||||
|
|
||||||
|
showMessage("Note(s) have been copied into clipboard.");
|
||||||
}
|
}
|
||||||
|
|
||||||
function cut(node) {
|
function cut(nodes) {
|
||||||
clipboardId = node.key;
|
clipboardIds = nodes.map(node => node.key);
|
||||||
clipboardMode = 'cut';
|
clipboardMode = 'cut';
|
||||||
|
|
||||||
|
showMessage("Note(s) have been cut into clipboard.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextMenuSettings = {
|
const contextMenuSettings = {
|
||||||
@@ -69,13 +87,16 @@ const contextMenu = (function() {
|
|||||||
{title: "Copy / clone <kbd>Ctrl+C</kbd>", cmd: "copy", uiIcon: "ui-icon-copy"},
|
{title: "Copy / clone <kbd>Ctrl+C</kbd>", cmd: "copy", uiIcon: "ui-icon-copy"},
|
||||||
{title: "Cut <kbd>Ctrl+X</kbd>", cmd: "cut", uiIcon: "ui-icon-scissors"},
|
{title: "Cut <kbd>Ctrl+X</kbd>", cmd: "cut", uiIcon: "ui-icon-scissors"},
|
||||||
{title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
|
{title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
|
||||||
{title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"}
|
{title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
|
||||||
|
{title: "----"},
|
||||||
|
{title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapse-sub-tree", uiIcon: "ui-icon-minus"},
|
||||||
|
{title: "Force note sync", cmd: "force-note-sync", uiIcon: "ui-icon-refresh"}
|
||||||
],
|
],
|
||||||
beforeOpen: (event, ui) => {
|
beforeOpen: (event, ui) => {
|
||||||
const node = $.ui.fancytree.getNode(ui.target);
|
const node = $.ui.fancytree.getNode(ui.target);
|
||||||
// Modify menu entries depending on node status
|
// Modify menu entries depending on node status
|
||||||
treeEl.contextmenu("enableEntry", "pasteAfter", clipboardId !== null);
|
treeEl.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0);
|
||||||
treeEl.contextmenu("enableEntry", "pasteInto", clipboardId !== null);
|
treeEl.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0);
|
||||||
|
|
||||||
// Activate node on right-click
|
// Activate node on right-click
|
||||||
node.setActive();
|
node.setActive();
|
||||||
@@ -106,10 +127,10 @@ const contextMenu = (function() {
|
|||||||
protected_session.protectSubTree(node.data.note_id, false);
|
protected_session.protectSubTree(node.data.note_id, false);
|
||||||
}
|
}
|
||||||
else if (ui.cmd === "copy") {
|
else if (ui.cmd === "copy") {
|
||||||
copy(node);
|
copy(noteTree.getSelectedNodes());
|
||||||
}
|
}
|
||||||
else if (ui.cmd === "cut") {
|
else if (ui.cmd === "cut") {
|
||||||
cut(node);
|
cut(noteTree.getSelectedNodes());
|
||||||
}
|
}
|
||||||
else if (ui.cmd === "pasteAfter") {
|
else if (ui.cmd === "pasteAfter") {
|
||||||
pasteAfter(node);
|
pasteAfter(node);
|
||||||
@@ -120,6 +141,12 @@ const contextMenu = (function() {
|
|||||||
else if (ui.cmd === "delete") {
|
else if (ui.cmd === "delete") {
|
||||||
treeChanges.deleteNode(node);
|
treeChanges.deleteNode(node);
|
||||||
}
|
}
|
||||||
|
else if (ui.cmd === "collapse-sub-tree") {
|
||||||
|
noteTree.collapseTree(node);
|
||||||
|
}
|
||||||
|
else if (ui.cmd === "force-note-sync") {
|
||||||
|
forceNoteSync(node.data.note_id);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
messaging.logError("Unknown command: " + ui.cmd);
|
messaging.logError("Unknown command: " + ui.cmd);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const editTreePrefix = (function() {
|
|||||||
|
|
||||||
await dialogEl.dialog({
|
await dialogEl.dialog({
|
||||||
modal: true,
|
modal: true,
|
||||||
width: 800
|
width: 500
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentNode = noteTree.getCurrentNode();
|
const currentNode = noteTree.getCurrentNode();
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const noteHistory = (function() {
|
|||||||
for (const item of historyItems) {
|
for (const item of historyItems) {
|
||||||
const dateModified = parseDate(item.date_modified_from);
|
const dateModified = parseDate(item.date_modified_from);
|
||||||
|
|
||||||
$("#note-history-list").append($('<option>', {
|
listEl.append($('<option>', {
|
||||||
value: item.note_history_id,
|
value: item.note_history_id,
|
||||||
text: formatDateTime(dateModified)
|
text: formatDateTime(dateModified)
|
||||||
}));
|
}));
|
||||||
@@ -42,6 +42,9 @@ const noteHistory = (function() {
|
|||||||
|
|
||||||
listEl.val(noteHistoryId).trigger('change');
|
listEl.val(noteHistoryId).trigger('change');
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
titleEl.text("No history for this note yet...");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).bind('keydown', 'alt+h', e => {
|
$(document).bind('keydown', 'alt+h', e => {
|
||||||
|
|||||||
57
public/javascripts/dialogs/note_source.js
Normal file
57
public/javascripts/dialogs/note_source.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const noteSource = (function() {
|
||||||
|
const dialogEl = $("#note-source-dialog");
|
||||||
|
const noteSourceEl = $("#note-source");
|
||||||
|
|
||||||
|
function showDialog() {
|
||||||
|
glob.activeDialog = dialogEl;
|
||||||
|
|
||||||
|
dialogEl.dialog({
|
||||||
|
modal: true,
|
||||||
|
width: 800,
|
||||||
|
height: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
const noteText = noteEditor.getCurrentNote().detail.note_text;
|
||||||
|
|
||||||
|
noteSourceEl.text(formatHtml(noteText));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatHtml(str) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.innerHTML = str.trim();
|
||||||
|
|
||||||
|
return formatNode(div, 0).innerHTML.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNode(node, level) {
|
||||||
|
const indentBefore = new Array(level++ + 1).join(' ');
|
||||||
|
const indentAfter = new Array(level - 1).join(' ');
|
||||||
|
let textNode;
|
||||||
|
|
||||||
|
for (let i = 0; i < node.children.length; i++) {
|
||||||
|
textNode = document.createTextNode('\n' + indentBefore);
|
||||||
|
node.insertBefore(textNode, node.children[i]);
|
||||||
|
|
||||||
|
formatNode(node.children[i], level);
|
||||||
|
|
||||||
|
if (node.lastElementChild === node.children[i]) {
|
||||||
|
textNode = document.createTextNode('\n' + indentAfter);
|
||||||
|
node.appendChild(textNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).bind('keydown', 'ctrl+u', e => {
|
||||||
|
showDialog();
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
showDialog
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -28,6 +28,9 @@ const sqlConsole = (function() {
|
|||||||
showError(result.error);
|
showError(result.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
showMessage("Query was executed successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
const rows = result.rows;
|
const rows = result.rows;
|
||||||
|
|
||||||
|
|||||||
@@ -46,14 +46,19 @@ const dragAndDropSetup = {
|
|||||||
// This function MUST be defined to enable dropping of items on the tree.
|
// This function MUST be defined to enable dropping of items on the tree.
|
||||||
// data.hitMode is 'before', 'after', or 'over'.
|
// data.hitMode is 'before', 'after', or 'over'.
|
||||||
|
|
||||||
|
const nodeToMove = data.otherNode;
|
||||||
|
nodeToMove.setSelected(true);
|
||||||
|
|
||||||
|
const selectedNodes = noteTree.getSelectedNodes();
|
||||||
|
|
||||||
if (data.hitMode === "before") {
|
if (data.hitMode === "before") {
|
||||||
treeChanges.moveBeforeNode(data.otherNode, node);
|
treeChanges.moveBeforeNode(selectedNodes, node);
|
||||||
}
|
}
|
||||||
else if (data.hitMode === "after") {
|
else if (data.hitMode === "after") {
|
||||||
treeChanges.moveAfterNode(data.otherNode, node);
|
treeChanges.moveAfterNode(selectedNodes, node);
|
||||||
}
|
}
|
||||||
else if (data.hitMode === "over") {
|
else if (data.hitMode === "over") {
|
||||||
treeChanges.moveToNode(data.otherNode, node);
|
treeChanges.moveToNode(selectedNodes, node);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new Exception("Unknown hitMode=" + data.hitMode);
|
throw new Exception("Unknown hitMode=" + data.hitMode);
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ jQuery.hotkeys.options.filterContentEditable = false;
|
|||||||
jQuery.hotkeys.options.filterTextInputs = false;
|
jQuery.hotkeys.options.filterTextInputs = false;
|
||||||
|
|
||||||
$(document).bind('keydown', 'alt+m', e => {
|
$(document).bind('keydown', 'alt+m', e => {
|
||||||
const toggle = $(".hide-toggle");
|
$(".hide-toggle").toggleClass("suppressed");
|
||||||
const hidden = toggle.css('visibility') === 'hidden';
|
|
||||||
|
|
||||||
toggle.css('visibility', hidden ? 'visible' : 'hidden');
|
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
@@ -49,7 +46,7 @@ $(document).bind('keydown', 'ctrl+f', () => {
|
|||||||
const searchInPage = require('electron-in-page-search').default;
|
const searchInPage = require('electron-in-page-search').default;
|
||||||
const remote = require('electron').remote;
|
const remote = require('electron').remote;
|
||||||
|
|
||||||
const inPageSearch = searchInPage(remote.getCurrentWebContents(), { openDevToolsOfSearchWindow: true });
|
const inPageSearch = searchInPage(remote.getCurrentWebContents());
|
||||||
|
|
||||||
inPageSearch.openSearchWindow();
|
inPageSearch.openSearchWindow();
|
||||||
|
|
||||||
@@ -57,6 +54,44 @@ $(document).bind('keydown', 'ctrl+f', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(document).bind('keydown', "ctrl+shift+left", () => {
|
||||||
|
const node = noteTree.getCurrentNode();
|
||||||
|
node.navigate($.ui.keyCode.LEFT, true);
|
||||||
|
|
||||||
|
$("#note-detail").focus();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).bind('keydown', "ctrl+shift+right", () => {
|
||||||
|
const node = noteTree.getCurrentNode();
|
||||||
|
node.navigate($.ui.keyCode.RIGHT, true);
|
||||||
|
|
||||||
|
$("#note-detail").focus();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).bind('keydown', "ctrl+shift+up", () => {
|
||||||
|
const node = noteTree.getCurrentNode();
|
||||||
|
node.navigate($.ui.keyCode.UP, true);
|
||||||
|
|
||||||
|
$("#note-detail").focus();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).bind('keydown', "ctrl+shift+down", () => {
|
||||||
|
const node = noteTree.getCurrentNode();
|
||||||
|
node.navigate($.ui.keyCode.DOWN, true);
|
||||||
|
|
||||||
|
$("#note-detail").focus();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#note-title").bind('keydown', 'return', () => $("#note-detail").focus());
|
||||||
|
|
||||||
$(window).on('beforeunload', () => {
|
$(window).on('beforeunload', () => {
|
||||||
// this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved
|
// this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved
|
||||||
// this sends the request asynchronously and doesn't wait for result
|
// this sends the request asynchronously and doesn't wait for result
|
||||||
@@ -122,27 +157,6 @@ $(document).tooltip({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let appShown = false;
|
|
||||||
|
|
||||||
function showAppIfHidden() {
|
|
||||||
if (!appShown) {
|
|
||||||
appShown = true;
|
|
||||||
|
|
||||||
$("#container").show();
|
|
||||||
|
|
||||||
// Get a reference to the loader's div
|
|
||||||
const loaderDiv = document.getElementById("loader-wrapper");
|
|
||||||
// When the transition ends remove loader's div from display
|
|
||||||
// so that we can access the map with gestures or clicks
|
|
||||||
loaderDiv.addEventListener("transitionend", function(){
|
|
||||||
loaderDiv.style.display = "none";
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
// Kick off the CSS transition
|
|
||||||
loaderDiv.style.opacity = 0.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||||
const string = msg.toLowerCase();
|
const string = msg.toLowerCase();
|
||||||
|
|
||||||
@@ -164,4 +178,6 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
|
|||||||
messaging.logError(message);
|
messaging.logError(message);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$("#logout-button").toggle(!isElectron());
|
||||||
@@ -42,16 +42,22 @@ const link = (function() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const linkEl = $(e.target);
|
const linkEl = $(e.target);
|
||||||
const notePath = linkEl.attr("note-path") ? linkEl.attr("note-path") : getNotePathFromLink(linkEl.attr('href'));
|
let notePath = linkEl.attr("note-path");
|
||||||
|
|
||||||
if (!notePath) {
|
if (!notePath) {
|
||||||
return;
|
const address = linkEl.attr("note-path") ? linkEl.attr("note-path") : linkEl.attr('href');
|
||||||
}
|
|
||||||
|
|
||||||
if (notePath.startsWith('http')) {
|
if (!address) {
|
||||||
window.open(notePath, '_blank');
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
if (address.startsWith('http')) {
|
||||||
|
window.open(address, '_blank');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
notePath = getNotePathFromLink(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
noteTree.activateNode(notePath);
|
noteTree.activateNode(notePath);
|
||||||
@@ -84,8 +90,8 @@ const link = (function() {
|
|||||||
// when click on link popup, in case of internal link, just go the the referenced note instead of default behavior
|
// when click on link popup, in case of internal link, just go the the referenced note instead of default behavior
|
||||||
// of opening the link in new window/tab
|
// of opening the link in new window/tab
|
||||||
$(document).on('click', "a[action='note']", goToLink);
|
$(document).on('click', "a[action='note']", goToLink);
|
||||||
$(document).on('click', 'div.popover-content a, div.ui-tooltip-content', goToLink);
|
$(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
|
||||||
$(document).on('dblclick', '#note-detail a, div.ui-tooltip-content', goToLink);
|
$(document).on('dblclick', '#note-detail a', goToLink);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getNodePathFromLabel,
|
getNodePathFromLabel,
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ const messaging = (function() {
|
|||||||
|
|
||||||
const syncData = message.data.filter(sync => sync.source_id !== glob.sourceId);
|
const syncData = message.data.filter(sync => sync.source_id !== glob.sourceId);
|
||||||
|
|
||||||
if (syncData.some(sync => sync.entity_name === 'notes_tree')) {
|
if (syncData.some(sync => sync.entity_name === 'notes_tree')
|
||||||
|
|| syncData.some(sync => sync.entity_name === 'notes')) {
|
||||||
|
|
||||||
console.log(now(), "Reloading tree because of background changes");
|
console.log(now(), "Reloading tree because of background changes");
|
||||||
|
|
||||||
noteTree.reload();
|
noteTree.reload();
|
||||||
@@ -47,6 +49,9 @@ const messaging = (function() {
|
|||||||
recentNotes.reload();
|
recentNotes.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we don't detect image changes here since images themselves are immutable and references should be
|
||||||
|
// updated in note detail as well
|
||||||
|
|
||||||
changesToPushCountEl.html(message.changesToPushCount);
|
changesToPushCountEl.html(message.changesToPushCount);
|
||||||
}
|
}
|
||||||
else if (message.type === 'sync-hash-check-failed') {
|
else if (message.type === 'sync-hash-check-failed') {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ const noteEditor = (function() {
|
|||||||
const protectButton = $("#protect-button");
|
const protectButton = $("#protect-button");
|
||||||
const unprotectButton = $("#unprotect-button");
|
const unprotectButton = $("#unprotect-button");
|
||||||
const noteDetailWrapperEl = $("#note-detail-wrapper");
|
const noteDetailWrapperEl = $("#note-detail-wrapper");
|
||||||
|
const noteIdDisplayEl = $("#note-id-display");
|
||||||
|
|
||||||
let editor = null;
|
let editor = null;
|
||||||
|
|
||||||
let currentNote = null;
|
let currentNote = null;
|
||||||
@@ -79,18 +81,11 @@ const noteEditor = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setNoteBackgroundIfProtected(note) {
|
function setNoteBackgroundIfProtected(note) {
|
||||||
if (note.detail.is_protected) {
|
const isProtected = !!note.detail.is_protected;
|
||||||
$("#note-detail-wrapper").addClass("protected");
|
|
||||||
protectButton.hide();
|
|
||||||
unprotectButton.show();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$("#note-detail-wrapper").removeClass("protected");
|
|
||||||
protectButton.show();
|
|
||||||
unprotectButton.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
noteTree.setCurrentNoteTreeBasedOnProtectedStatus();
|
noteDetailWrapperEl.toggleClass("protected", isProtected);
|
||||||
|
protectButton.toggle(!isProtected);
|
||||||
|
unprotectButton.toggle(isProtected);
|
||||||
}
|
}
|
||||||
|
|
||||||
let isNewNoteCreated = false;
|
let isNewNoteCreated = false;
|
||||||
@@ -108,6 +103,8 @@ const noteEditor = (function() {
|
|||||||
noteTitleEl.focus().select();
|
noteTitleEl.focus().select();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
noteIdDisplayEl.html(noteId);
|
||||||
|
|
||||||
await protected_session.ensureProtectedSession(currentNote.detail.is_protected, false);
|
await protected_session.ensureProtectedSession(currentNote.detail.is_protected, false);
|
||||||
|
|
||||||
if (currentNote.detail.is_protected) {
|
if (currentNote.detail.is_protected) {
|
||||||
@@ -129,8 +126,10 @@ const noteEditor = (function() {
|
|||||||
noteChangeDisabled = false;
|
noteChangeDisabled = false;
|
||||||
|
|
||||||
setNoteBackgroundIfProtected(currentNote);
|
setNoteBackgroundIfProtected(currentNote);
|
||||||
|
noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
|
||||||
|
|
||||||
showAppIfHidden();
|
// after loading new note make sure editor is scrolled to the top
|
||||||
|
noteDetailWrapperEl.scrollTop(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadNote(noteId) {
|
async function loadNote(noteId) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const noteTree = (function() {
|
|||||||
const treeEl = $("#tree");
|
const treeEl = $("#tree");
|
||||||
const parentListEl = $("#parent-list");
|
const parentListEl = $("#parent-list");
|
||||||
const parentListListEl = $("#parent-list-list");
|
const parentListListEl = $("#parent-list-list");
|
||||||
|
const noteDetailEl = $("#note-detail");
|
||||||
|
|
||||||
let startNotePath = null;
|
let startNotePath = null;
|
||||||
let notesTreeMap = {};
|
let notesTreeMap = {};
|
||||||
@@ -59,23 +60,6 @@ const noteTree = (function() {
|
|||||||
return treeUtils.getNotePath(node);
|
return treeUtils.getNotePath(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentNoteId() {
|
|
||||||
const node = getCurrentNode();
|
|
||||||
|
|
||||||
return node ? node.data.note_id : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentClones() {
|
|
||||||
const noteId = getCurrentNoteId();
|
|
||||||
|
|
||||||
if (noteId) {
|
|
||||||
return getNodesByNoteId(noteId);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNodesByNoteTreeId(noteTreeId) {
|
function getNodesByNoteTreeId(noteTreeId) {
|
||||||
assertArguments(noteTreeId);
|
assertArguments(noteTreeId);
|
||||||
|
|
||||||
@@ -157,21 +141,17 @@ const noteTree = (function() {
|
|||||||
function getExtraClasses(note) {
|
function getExtraClasses(note) {
|
||||||
assertArguments(note);
|
assertArguments(note);
|
||||||
|
|
||||||
let extraClasses = '';
|
const extraClasses = [];
|
||||||
|
|
||||||
if (note.is_protected) {
|
if (note.is_protected) {
|
||||||
extraClasses += ",protected";
|
extraClasses.push("protected");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (childToParents[note.note_id].length > 1) {
|
if (childToParents[note.note_id].length > 1) {
|
||||||
extraClasses += ",multiple-parents";
|
extraClasses.push("multiple-parents");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extraClasses.startsWith(",")) {
|
return extraClasses.join(" ");
|
||||||
extraClasses = extraClasses.substr(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return extraClasses;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareNoteTreeInner(parentNoteId) {
|
function prepareNoteTreeInner(parentNoteId) {
|
||||||
@@ -189,13 +169,15 @@ const noteTree = (function() {
|
|||||||
const noteTreeId = getNoteTreeId(parentNoteId, noteId);
|
const noteTreeId = getNoteTreeId(parentNoteId, noteId);
|
||||||
const noteTree = notesTreeMap[noteTreeId];
|
const noteTree = notesTreeMap[noteTreeId];
|
||||||
|
|
||||||
|
const title = (noteTree.prefix ? (noteTree.prefix + " - ") : "") + noteIdToTitle[noteTree.note_id];
|
||||||
|
|
||||||
const node = {
|
const node = {
|
||||||
note_id: noteTree.note_id,
|
note_id: noteTree.note_id,
|
||||||
parent_note_id: noteTree.parent_note_id,
|
parent_note_id: noteTree.parent_note_id,
|
||||||
note_tree_id: noteTree.note_tree_id,
|
note_tree_id: noteTree.note_tree_id,
|
||||||
is_protected: noteTree.is_protected,
|
is_protected: noteTree.is_protected,
|
||||||
prefix: noteTree.prefix,
|
prefix: noteTree.prefix,
|
||||||
title: (noteTree.prefix ? (noteTree.prefix + " - ") : "") + noteIdToTitle[noteTree.note_id],
|
title: escapeHtml(title),
|
||||||
extraClasses: getExtraClasses(noteTree),
|
extraClasses: getExtraClasses(noteTree),
|
||||||
refKey: noteTree.note_id,
|
refKey: noteTree.note_id,
|
||||||
expanded: noteTree.is_expanded
|
expanded: noteTree.is_expanded
|
||||||
@@ -227,8 +209,6 @@ const noteTree = (function() {
|
|||||||
|
|
||||||
let parentNoteId = 'root';
|
let parentNoteId = 'root';
|
||||||
|
|
||||||
//console.log(now(), "Run path: ", runPath);
|
|
||||||
|
|
||||||
for (const childNoteId of runPath) {
|
for (const childNoteId of runPath) {
|
||||||
const node = getNodesByNoteId(childNoteId).find(node => node.data.parent_note_id === parentNoteId);
|
const node = getNodesByNoteId(childNoteId).find(node => node.data.parent_note_id === parentNoteId);
|
||||||
|
|
||||||
@@ -241,6 +221,8 @@ const noteTree = (function() {
|
|||||||
|
|
||||||
parentNoteId = childNoteId;
|
parentNoteId = childNoteId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearSelectedNodes();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -400,6 +382,22 @@ const noteTree = (function() {
|
|||||||
recentNotes.addRecentNote(currentNoteTreeId, currentNotePath);
|
recentNotes.addRecentNote(currentNoteTreeId, currentNotePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSelectedNodes() {
|
||||||
|
return getTree().getSelectedNodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelectedNodes() {
|
||||||
|
for (const selectedNode of getSelectedNodes()) {
|
||||||
|
selectedNode.setSelected(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentNode = getCurrentNode();
|
||||||
|
|
||||||
|
if (currentNode) {
|
||||||
|
currentNode.setSelected(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initFancyTree(noteTree) {
|
function initFancyTree(noteTree) {
|
||||||
assertArguments(noteTree);
|
assertArguments(noteTree);
|
||||||
|
|
||||||
@@ -407,46 +405,121 @@ const noteTree = (function() {
|
|||||||
"del": node => {
|
"del": node => {
|
||||||
treeChanges.deleteNode(node);
|
treeChanges.deleteNode(node);
|
||||||
},
|
},
|
||||||
"shift+up": node => {
|
"ctrl+up": node => {
|
||||||
const beforeNode = node.getPrevSibling();
|
const beforeNode = node.getPrevSibling();
|
||||||
|
|
||||||
if (beforeNode !== null) {
|
if (beforeNode !== null) {
|
||||||
treeChanges.moveBeforeNode(node, beforeNode);
|
treeChanges.moveBeforeNode([node], beforeNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
"shift+down": node => {
|
"ctrl+down": node => {
|
||||||
let afterNode = node.getNextSibling();
|
let afterNode = node.getNextSibling();
|
||||||
if (afterNode !== null) {
|
if (afterNode !== null) {
|
||||||
treeChanges.moveAfterNode(node, afterNode);
|
treeChanges.moveAfterNode([node], afterNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
"shift+left": node => {
|
"ctrl+left": node => {
|
||||||
treeChanges.moveNodeUpInHierarchy(node);
|
treeChanges.moveNodeUpInHierarchy(node);
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
"shift+right": node => {
|
"ctrl+right": node => {
|
||||||
let toNode = node.getPrevSibling();
|
let toNode = node.getPrevSibling();
|
||||||
|
|
||||||
if (toNode !== null) {
|
if (toNode !== null) {
|
||||||
treeChanges.moveToNode(node, toNode);
|
treeChanges.moveToNode([node], toNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
"shift+up": node => {
|
||||||
|
node.navigate($.ui.keyCode.UP, true).then(() => {
|
||||||
|
const currentNode = getCurrentNode();
|
||||||
|
|
||||||
|
if (currentNode.isSelected()) {
|
||||||
|
node.setSelected(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentNode.setSelected(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
"shift+down": node => {
|
||||||
|
node.navigate($.ui.keyCode.DOWN, true).then(() => {
|
||||||
|
const currentNode = getCurrentNode();
|
||||||
|
|
||||||
|
if (currentNode.isSelected()) {
|
||||||
|
node.setSelected(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentNode.setSelected(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
"f2": node => {
|
"f2": node => {
|
||||||
editTreePrefix.showDialog(node);
|
editTreePrefix.showDialog(node);
|
||||||
},
|
},
|
||||||
|
"alt+-": node => {
|
||||||
|
collapseTree(node);
|
||||||
|
},
|
||||||
|
"ctrl+a": node => {
|
||||||
|
for (const child of node.getParent().getChildren()) {
|
||||||
|
child.setSelected(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
"ctrl+c": () => {
|
||||||
|
contextMenu.copy(getSelectedNodes());
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
"ctrl+x": () => {
|
||||||
|
contextMenu.cut(getSelectedNodes());
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
"ctrl+v": node => {
|
||||||
|
contextMenu.pasteInto(node);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
"return": node => {
|
||||||
|
noteDetailEl.focus();
|
||||||
|
},
|
||||||
|
"backspace": node => {
|
||||||
|
if (!isTopLevelNode(node)) {
|
||||||
|
node.getParent().setActive().then(() => clearSelectedNodes());
|
||||||
|
}
|
||||||
|
},
|
||||||
// code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin
|
// code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin
|
||||||
// after opening context menu, standard shortcuts don't work, but they are detected here
|
// after opening context menu, standard shortcuts don't work, but they are detected here
|
||||||
// so we essentially takeover the standard handling with our implementation.
|
// so we essentially takeover the standard handling with our implementation.
|
||||||
"left": node => {
|
"left": node => {
|
||||||
node.navigate($.ui.keyCode.LEFT, true);
|
node.navigate($.ui.keyCode.LEFT, true).then(() => clearSelectedNodes());
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
"right": node => {
|
"right": node => {
|
||||||
node.navigate($.ui.keyCode.RIGHT, true);
|
node.navigate($.ui.keyCode.RIGHT, true).then(() => clearSelectedNodes());
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
"up": node => {
|
"up": node => {
|
||||||
node.navigate($.ui.keyCode.UP, true);
|
node.navigate($.ui.keyCode.UP, true).then(() => clearSelectedNodes());
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
"down": node => {
|
"down": node => {
|
||||||
node.navigate($.ui.keyCode.DOWN, true);
|
node.navigate($.ui.keyCode.DOWN, true).then(() => clearSelectedNodes());
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -456,6 +529,24 @@ const noteTree = (function() {
|
|||||||
extensions: ["hotkeys", "filter", "dnd", "clones"],
|
extensions: ["hotkeys", "filter", "dnd", "clones"],
|
||||||
source: noteTree,
|
source: noteTree,
|
||||||
scrollParent: $("#tree"),
|
scrollParent: $("#tree"),
|
||||||
|
click: (event, data) => {
|
||||||
|
const targetType = data.targetType;
|
||||||
|
const node = data.node;
|
||||||
|
|
||||||
|
if (targetType === 'title' || targetType === 'icon') {
|
||||||
|
if (!event.ctrlKey) {
|
||||||
|
node.setActive();
|
||||||
|
node.setSelected(true);
|
||||||
|
|
||||||
|
clearSelectedNodes();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
node.setSelected(!node.isSelected());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
activate: (event, data) => {
|
activate: (event, data) => {
|
||||||
const node = data.node.data;
|
const node = data.node.data;
|
||||||
|
|
||||||
@@ -486,9 +577,6 @@ const noteTree = (function() {
|
|||||||
// so waiting a second helps
|
// so waiting a second helps
|
||||||
setTimeout(scrollToCurrentNote, 1000);
|
setTimeout(scrollToCurrentNote, 1000);
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
showAppIfHidden();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
hotkeys: {
|
hotkeys: {
|
||||||
keydown: keybindings
|
keydown: keybindings
|
||||||
@@ -506,57 +594,6 @@ const noteTree = (function() {
|
|||||||
mode: "hide" // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
|
mode: "hide" // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
|
||||||
},
|
},
|
||||||
dnd: dragAndDropSetup,
|
dnd: dragAndDropSetup,
|
||||||
keydown: (event, data) => {
|
|
||||||
const node = data.node;
|
|
||||||
// Eat keyboard events, when a menu is open
|
|
||||||
if ($(".contextMenu:visible").length > 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
switch (event.which) {
|
|
||||||
// Open context menu on [Space] key (simulate right click)
|
|
||||||
case 32: // [Space]
|
|
||||||
$(node.span).trigger("mousedown", {
|
|
||||||
preventDefault: true,
|
|
||||||
button: 2
|
|
||||||
})
|
|
||||||
.trigger("mouseup", {
|
|
||||||
preventDefault: true,
|
|
||||||
pageX: node.span.offsetLeft,
|
|
||||||
pageY: node.span.offsetTop,
|
|
||||||
button: 2
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Handle Ctrl-C, -X and -V
|
|
||||||
case 67:
|
|
||||||
if (event.ctrlKey) { // Ctrl-C
|
|
||||||
contextMenu.copy(node);
|
|
||||||
|
|
||||||
showMessage("Note copied into clipboard.");
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 88:
|
|
||||||
if (event.ctrlKey) { // Ctrl-X
|
|
||||||
contextMenu.cut(node);
|
|
||||||
|
|
||||||
showMessage("Note cut into clipboard.");
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 86:
|
|
||||||
if (event.ctrlKey) { // Ctrl-V
|
|
||||||
contextMenu.pasteInto(node);
|
|
||||||
|
|
||||||
showMessage("Note pasted from clipboard into current note.");
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
lazyLoad: function(event, data){
|
lazyLoad: function(event, data){
|
||||||
const node = data.node.data;
|
const node = data.node.data;
|
||||||
|
|
||||||
@@ -599,13 +636,17 @@ const noteTree = (function() {
|
|||||||
|
|
||||||
$(() => loadTree().then(noteTree => initFancyTree(noteTree)));
|
$(() => loadTree().then(noteTree => initFancyTree(noteTree)));
|
||||||
|
|
||||||
function collapseTree() {
|
function collapseTree(node = null) {
|
||||||
treeEl.fancytree("getRootNode").visit(node => {
|
if (!node) {
|
||||||
node.setExpanded(false);
|
node = treeEl.fancytree("getRootNode");
|
||||||
});
|
}
|
||||||
|
|
||||||
|
node.setExpanded(false);
|
||||||
|
|
||||||
|
node.visit(node => node.setExpanded(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).bind('keydown', 'alt+c', collapseTree);
|
$(document).bind('keydown', 'alt+c', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument
|
||||||
|
|
||||||
function scrollToCurrentNote() {
|
function scrollToCurrentNote() {
|
||||||
const node = getCurrentNode();
|
const node = getCurrentNode();
|
||||||
@@ -617,8 +658,14 @@ const noteTree = (function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCurrentNoteTreeBasedOnProtectedStatus() {
|
function setNoteTreeBackgroundBasedOnProtectedStatus(noteId) {
|
||||||
getCurrentClones().map(node => node.toggleClass("protected", !!node.data.is_protected));
|
getNodesByNoteId(noteId).map(node => node.toggleClass("protected", !!node.data.is_protected));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProtected(noteId, isProtected) {
|
||||||
|
getNodesByNoteId(noteId).map(node => node.data.is_protected = isProtected);
|
||||||
|
|
||||||
|
setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAutocompleteItems(parentNoteId, notePath, titlePath) {
|
function getAutocompleteItems(parentNoteId, notePath, titlePath) {
|
||||||
@@ -755,7 +802,11 @@ const noteTree = (function() {
|
|||||||
$(window).bind('hashchange', function() {
|
$(window).bind('hashchange', function() {
|
||||||
const notePath = getNotePathFromAddress();
|
const notePath = getNotePathFromAddress();
|
||||||
|
|
||||||
activateNode(notePath);
|
if (getCurrentNotePath() !== notePath) {
|
||||||
|
console.log("Switching to " + notePath + " because of hash change");
|
||||||
|
|
||||||
|
activateNode(notePath);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
@@ -776,7 +827,8 @@ const noteTree = (function() {
|
|||||||
reload,
|
reload,
|
||||||
collapseTree,
|
collapseTree,
|
||||||
scrollToCurrentNote,
|
scrollToCurrentNote,
|
||||||
setCurrentNoteTreeBasedOnProtectedStatus,
|
setNoteTreeBackgroundBasedOnProtectedStatus,
|
||||||
|
setProtected,
|
||||||
getCurrentNode,
|
getCurrentNode,
|
||||||
activateNode,
|
activateNode,
|
||||||
getCurrentNotePath,
|
getCurrentNotePath,
|
||||||
@@ -789,6 +841,7 @@ const noteTree = (function() {
|
|||||||
setPrefix,
|
setPrefix,
|
||||||
getNotePathTitle,
|
getNotePathTitle,
|
||||||
removeParentChildRelation,
|
removeParentChildRelation,
|
||||||
setParentChildRelation
|
setParentChildRelation,
|
||||||
|
getSelectedNodes
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
@@ -22,9 +22,6 @@ const protected_session = (function() {
|
|||||||
const dfd = $.Deferred();
|
const dfd = $.Deferred();
|
||||||
|
|
||||||
if (requireProtectedSession && !isProtectedSessionAvailable()) {
|
if (requireProtectedSession && !isProtectedSessionAvailable()) {
|
||||||
// if this is entry point then we need to show the app even before the note is loaded
|
|
||||||
showAppIfHidden();
|
|
||||||
|
|
||||||
protectedSessionDeferred = dfd;
|
protectedSessionDeferred = dfd;
|
||||||
|
|
||||||
dialogEl.dialog({
|
dialogEl.dialog({
|
||||||
@@ -115,6 +112,8 @@ const protected_session = (function() {
|
|||||||
|
|
||||||
await noteEditor.saveNoteToServer(note);
|
await noteEditor.saveNoteToServer(note);
|
||||||
|
|
||||||
|
noteTree.setProtected(note.detail.note_id, note.detail.is_protected);
|
||||||
|
|
||||||
noteEditor.setNoteBackgroundIfProtected(note);
|
noteEditor.setNoteBackgroundIfProtected(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +128,8 @@ const protected_session = (function() {
|
|||||||
|
|
||||||
await noteEditor.saveNoteToServer(note);
|
await noteEditor.saveNoteToServer(note);
|
||||||
|
|
||||||
|
noteTree.setProtected(note.detail.note_id, note.detail.is_protected);
|
||||||
|
|
||||||
noteEditor.setNoteBackgroundIfProtected(note);
|
noteEditor.setNoteBackgroundIfProtected(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,10 @@ const searchTree = (function() {
|
|||||||
|
|
||||||
resetSearchButton.click(resetSearch);
|
resetSearchButton.click(resetSearch);
|
||||||
|
|
||||||
function showSearch() {
|
|
||||||
searchBoxEl.show();
|
|
||||||
searchInputEl.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSearch() {
|
function toggleSearch() {
|
||||||
if (searchBoxEl.is(":hidden")) {
|
if (searchBoxEl.is(":hidden")) {
|
||||||
showSearch();
|
searchBoxEl.show();
|
||||||
|
searchInputEl.focus();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
resetSearch();
|
resetSearch();
|
||||||
@@ -52,7 +48,11 @@ const searchTree = (function() {
|
|||||||
}
|
}
|
||||||
}).focus();
|
}).focus();
|
||||||
|
|
||||||
$(document).bind('keydown', 'alt+s', showSearch);
|
$(document).bind('keydown', 'alt+s', e => {
|
||||||
|
toggleSearch();
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toggleSearch
|
toggleSearch
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ const server = (function() {
|
|||||||
get,
|
get,
|
||||||
post,
|
post,
|
||||||
put,
|
put,
|
||||||
remove
|
remove,
|
||||||
|
getHeaders
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -13,4 +13,10 @@ async function syncNow() {
|
|||||||
|
|
||||||
showError("Sync failed: " + result.message);
|
showError("Sync failed: " + result.message);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function forceNoteSync(noteId) {
|
||||||
|
const result = await server.post('sync/force-note-sync/' + noteId);
|
||||||
|
|
||||||
|
showMessage("Note added to sync queue.");
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,30 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const treeChanges = (function() {
|
const treeChanges = (function() {
|
||||||
async function moveBeforeNode(node, beforeNode) {
|
async function moveBeforeNode(nodesToMove, beforeNode) {
|
||||||
await server.put('notes/' + node.data.note_tree_id + '/move-before/' + beforeNode.data.note_tree_id);
|
for (const nodeToMove of nodesToMove) {
|
||||||
|
const resp = await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-before/' + beforeNode.data.note_tree_id);
|
||||||
|
|
||||||
changeNode(node, node => node.moveTo(beforeNode, 'before'));
|
if (!resp.success) {
|
||||||
|
alert(resp.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeNode(nodeToMove, node => node.moveTo(beforeNode, 'before'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function moveAfterNode(node, afterNode) {
|
async function moveAfterNode(nodesToMove, afterNode) {
|
||||||
await server.put('notes/' + node.data.note_tree_id + '/move-after/' + afterNode.data.note_tree_id);
|
for (const nodeToMove of nodesToMove) {
|
||||||
|
const resp = await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-after/' + afterNode.data.note_tree_id);
|
||||||
|
|
||||||
changeNode(node, node => node.moveTo(afterNode, 'after'));
|
if (!resp.success) {
|
||||||
|
alert(resp.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeNode(nodeToMove, node => node.moveTo(afterNode, 'after'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// beware that first arg is noteId and second is noteTreeId!
|
// beware that first arg is noteId and second is noteTreeId!
|
||||||
@@ -25,17 +39,30 @@ const treeChanges = (function() {
|
|||||||
await noteTree.reload();
|
await noteTree.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function moveToNode(node, toNode) {
|
async function moveToNode(nodesToMove, toNode) {
|
||||||
await server.put('notes/' + node.data.note_tree_id + '/move-to/' + toNode.data.note_id);
|
for (const nodeToMove of nodesToMove) {
|
||||||
|
const resp = await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-to/' + toNode.data.note_id);
|
||||||
|
|
||||||
changeNode(node, node => {
|
if (!resp.success) {
|
||||||
node.moveTo(toNode);
|
alert(resp.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
toNode.setExpanded(true);
|
changeNode(nodeToMove, node => {
|
||||||
|
// first expand which will force lazy load and only then move the node
|
||||||
|
// if this is not expanded before moving, then lazy load won't happen because it already contains node
|
||||||
|
// this doesn't work if this isn't a folder yet, that's why we expand second time below
|
||||||
|
toNode.setExpanded(true);
|
||||||
|
|
||||||
toNode.folder = true;
|
node.moveTo(toNode);
|
||||||
toNode.renderTitle();
|
|
||||||
});
|
toNode.folder = true;
|
||||||
|
toNode.renderTitle();
|
||||||
|
|
||||||
|
// this expands the note in case it become the folder only after the move
|
||||||
|
toNode.setExpanded(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cloneNoteTo(childNoteId, parentNoteId, prefix) {
|
async function cloneNoteTo(childNoteId, parentNoteId, prefix) {
|
||||||
@@ -52,7 +79,7 @@ const treeChanges = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteNode(node) {
|
async function deleteNode(node) {
|
||||||
if (!confirm('Are you sure you want to delete note "' + node.title + '"?')) {
|
if (!confirm('Are you sure you want to delete note "' + node.title + '" and all its sub-notes?')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +110,8 @@ const treeChanges = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
noteTree.reload();
|
noteTree.reload();
|
||||||
|
|
||||||
|
showMessage("Note has been deleted.");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function moveNodeUpInHierarchy(node) {
|
async function moveNodeUpInHierarchy(node) {
|
||||||
@@ -90,7 +119,12 @@ const treeChanges = (function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await server.put('notes/' + node.data.note_tree_id + '/move-after/' + node.getParent().data.note_tree_id);
|
const resp = await server.put('notes/' + node.data.note_tree_id + '/move-after/' + node.getParent().data.note_tree_id);
|
||||||
|
|
||||||
|
if (!resp.success) {
|
||||||
|
alert(resp.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isTopLevelNode(node) && node.getParent().getChildren().length <= 1) {
|
if (!isTopLevelNode(node) && node.getParent().getChildren().length <= 1) {
|
||||||
node.getParent().folder = false;
|
node.getParent().folder = false;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const treeUtils = (function() {
|
|||||||
|
|
||||||
const title = (prefix ? (prefix + " - ") : "") + noteTitle;
|
const title = (prefix ? (prefix + " - ") : "") + noteTitle;
|
||||||
|
|
||||||
node.setTitle(title);
|
node.setTitle(escapeHtml(title));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -93,4 +93,8 @@ function isTopLevelNode(node) {
|
|||||||
|
|
||||||
function isRootNode(node) {
|
function isRootNode(node) {
|
||||||
return node.key === "root_1";
|
return node.key === "root_1";
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return $('<div/>').text(str).html();
|
||||||
}
|
}
|
||||||
4
public/libraries/ckeditor/ckeditor.js
vendored
4
public/libraries/ckeditor/ckeditor.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
631
public/libraries/jquery.ui-contextmenu.js
Normal file
631
public/libraries/jquery.ui-contextmenu.js
Normal file
@@ -0,0 +1,631 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* jquery.ui-contextmenu.js plugin.
|
||||||
|
*
|
||||||
|
* jQuery plugin that provides a context menu (based on the jQueryUI menu widget).
|
||||||
|
*
|
||||||
|
* @see https://github.com/mar10/jquery-ui-contextmenu
|
||||||
|
*
|
||||||
|
* Copyright (c) 2013-2017, Martin Wendt (http://wwWendt.de). Licensed MIT.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function( factory ) {
|
||||||
|
"use strict";
|
||||||
|
if ( typeof define === "function" && define.amd ) {
|
||||||
|
// AMD. Register as an anonymous module.
|
||||||
|
define([ "jquery", "jquery-ui/ui/widgets/menu" ], factory );
|
||||||
|
} else {
|
||||||
|
// Browser globals
|
||||||
|
factory( jQuery );
|
||||||
|
}
|
||||||
|
}(function( $ ) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var supportSelectstart = "onselectstart" in document.createElement("div"),
|
||||||
|
match = $.ui.menu.version.match(/^(\d)\.(\d+)/),
|
||||||
|
uiVersion = {
|
||||||
|
major: parseInt(match[1], 10),
|
||||||
|
minor: parseInt(match[2], 10)
|
||||||
|
},
|
||||||
|
isLTE110 = ( uiVersion.major < 2 && uiVersion.minor <= 10 ),
|
||||||
|
isLTE111 = ( uiVersion.major < 2 && uiVersion.minor <= 11 );
|
||||||
|
|
||||||
|
$.widget("moogle.contextmenu", {
|
||||||
|
version: "@VERSION",
|
||||||
|
options: {
|
||||||
|
addClass: "ui-contextmenu", // Add this class to the outer <ul>
|
||||||
|
closeOnWindowBlur: true, // Close menu when window loses focus
|
||||||
|
autoFocus: false, // Set keyboard focus to first entry on open
|
||||||
|
autoTrigger: true, // open menu on browser's `contextmenu` event
|
||||||
|
delegate: null, // selector
|
||||||
|
hide: { effect: "fadeOut", duration: "fast" },
|
||||||
|
ignoreParentSelect: true, // Don't trigger 'select' for sub-menu parents
|
||||||
|
menu: null, // selector or jQuery pointing to <UL>, or a definition hash
|
||||||
|
position: null, // popup positon
|
||||||
|
preventContextMenuForPopup: false, // prevent opening the browser's system
|
||||||
|
// context menu on menu entries
|
||||||
|
preventSelect: false, // disable text selection of target
|
||||||
|
show: { effect: "slideDown", duration: "fast" },
|
||||||
|
taphold: false, // open menu on taphold events (requires external plugins)
|
||||||
|
uiMenuOptions: {}, // Additional options, used when UI Menu is created
|
||||||
|
// Events:
|
||||||
|
beforeOpen: $.noop, // menu about to open; return `false` to prevent opening
|
||||||
|
blur: $.noop, // menu option lost focus
|
||||||
|
close: $.noop, // menu was closed
|
||||||
|
create: $.noop, // menu was initialized
|
||||||
|
createMenu: $.noop, // menu was initialized (original UI Menu)
|
||||||
|
focus: $.noop, // menu option got focus
|
||||||
|
open: $.noop, // menu was opened
|
||||||
|
select: $.noop // menu option was selected; return `false` to prevent closing
|
||||||
|
},
|
||||||
|
/** Constructor */
|
||||||
|
_create: function() {
|
||||||
|
var cssText, eventNames, targetId,
|
||||||
|
opts = this.options;
|
||||||
|
|
||||||
|
this.$headStyle = null;
|
||||||
|
this.$menu = null;
|
||||||
|
this.menuIsTemp = false;
|
||||||
|
this.currentTarget = null;
|
||||||
|
this.extraData = {};
|
||||||
|
this.previousFocus = null;
|
||||||
|
|
||||||
|
if (opts.delegate == null) {
|
||||||
|
$.error("ui-contextmenu: Missing required option `delegate`.");
|
||||||
|
}
|
||||||
|
if (opts.preventSelect) {
|
||||||
|
// Create a global style for all potential menu targets
|
||||||
|
// If the contextmenu was bound to `document`, we apply the
|
||||||
|
// selector relative to the <body> tag instead
|
||||||
|
targetId = ($(this.element).is(document) ? $("body")
|
||||||
|
: this.element).uniqueId().attr("id");
|
||||||
|
cssText = "#" + targetId + " " + opts.delegate + " { " +
|
||||||
|
"-webkit-user-select: none; " +
|
||||||
|
"-khtml-user-select: none; " +
|
||||||
|
"-moz-user-select: none; " +
|
||||||
|
"-ms-user-select: none; " +
|
||||||
|
"user-select: none; " +
|
||||||
|
"}";
|
||||||
|
this.$headStyle = $("<style class='moogle-contextmenu-style' />")
|
||||||
|
.prop("type", "text/css")
|
||||||
|
.appendTo("head");
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.$headStyle.html(cssText);
|
||||||
|
} catch ( e ) {
|
||||||
|
// issue #47: fix for IE 6-8
|
||||||
|
this.$headStyle[0].styleSheet.cssText = cssText;
|
||||||
|
}
|
||||||
|
// TODO: the selectstart is not supported by FF?
|
||||||
|
if (supportSelectstart) {
|
||||||
|
this.element.on("selectstart" + this.eventNamespace, opts.delegate,
|
||||||
|
function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._createUiMenu(opts.menu);
|
||||||
|
|
||||||
|
eventNames = "contextmenu" + this.eventNamespace;
|
||||||
|
if (opts.taphold) {
|
||||||
|
eventNames += " taphold" + this.eventNamespace;
|
||||||
|
}
|
||||||
|
this.element.on(eventNames, opts.delegate, $.proxy(this._openMenu, this));
|
||||||
|
},
|
||||||
|
/** Destructor, called on $().contextmenu("destroy"). */
|
||||||
|
_destroy: function() {
|
||||||
|
this.element.off(this.eventNamespace);
|
||||||
|
|
||||||
|
this._createUiMenu(null);
|
||||||
|
|
||||||
|
if (this.$headStyle) {
|
||||||
|
this.$headStyle.remove();
|
||||||
|
this.$headStyle = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** (Re)Create jQuery UI Menu. */
|
||||||
|
_createUiMenu: function(menuDef) {
|
||||||
|
var ct, ed,
|
||||||
|
opts = this.options;
|
||||||
|
|
||||||
|
// Remove temporary <ul> if any
|
||||||
|
if (this.isOpen()) {
|
||||||
|
// #58: 'replaceMenu' in beforeOpen causing select: to lose ui.target
|
||||||
|
ct = this.currentTarget;
|
||||||
|
ed = this.extraData;
|
||||||
|
// close without animation, to force async mode
|
||||||
|
this._closeMenu(true);
|
||||||
|
this.currentTarget = ct;
|
||||||
|
this.extraData = ed;
|
||||||
|
}
|
||||||
|
if (this.menuIsTemp) {
|
||||||
|
this.$menu.remove(); // this will also destroy ui.menu
|
||||||
|
} else if (this.$menu) {
|
||||||
|
this.$menu
|
||||||
|
.menu("destroy")
|
||||||
|
.removeClass(this.options.addClass)
|
||||||
|
.hide();
|
||||||
|
}
|
||||||
|
this.$menu = null;
|
||||||
|
this.menuIsTemp = false;
|
||||||
|
// If a menu definition array was passed, create a hidden <ul>
|
||||||
|
// and generate the structure now
|
||||||
|
if ( !menuDef ) {
|
||||||
|
return;
|
||||||
|
} else if ($.isArray(menuDef)) {
|
||||||
|
this.$menu = $.moogle.contextmenu.createMenuMarkup(menuDef);
|
||||||
|
this.menuIsTemp = true;
|
||||||
|
}else if ( typeof menuDef === "string" ) {
|
||||||
|
this.$menu = $(menuDef);
|
||||||
|
} else {
|
||||||
|
this.$menu = menuDef;
|
||||||
|
}
|
||||||
|
// Create - but hide - the jQuery UI Menu widget
|
||||||
|
this.$menu
|
||||||
|
.hide()
|
||||||
|
.addClass(opts.addClass)
|
||||||
|
// Create a menu instance that delegates events to our widget
|
||||||
|
.menu($.extend(true, {}, opts.uiMenuOptions, {
|
||||||
|
items: "> :not(.ui-widget-header)",
|
||||||
|
blur: $.proxy(opts.blur, this),
|
||||||
|
create: $.proxy(opts.createMenu, this),
|
||||||
|
focus: $.proxy(opts.focus, this),
|
||||||
|
select: $.proxy(function(event, ui) {
|
||||||
|
// User selected a menu entry
|
||||||
|
var retval,
|
||||||
|
isParent = $.moogle.contextmenu.isMenu(ui.item),
|
||||||
|
actionHandler = ui.item.data("actionHandler");
|
||||||
|
|
||||||
|
ui.cmd = ui.item.attr("data-command");
|
||||||
|
ui.target = $(this.currentTarget);
|
||||||
|
ui.extraData = this.extraData;
|
||||||
|
// ignore clicks, if they only open a sub-menu
|
||||||
|
if ( !isParent || !opts.ignoreParentSelect) {
|
||||||
|
retval = this._trigger.call(this, "select", event, ui);
|
||||||
|
if ( actionHandler ) {
|
||||||
|
retval = actionHandler.call(this, event, ui);
|
||||||
|
}
|
||||||
|
if ( retval !== false ) {
|
||||||
|
this._closeMenu.call(this);
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}, this)
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
/** Open popup (called on 'contextmenu' event). */
|
||||||
|
_openMenu: function(event, recursive) {
|
||||||
|
var res, promise, ui,
|
||||||
|
opts = this.options,
|
||||||
|
posOption = opts.position,
|
||||||
|
self = this,
|
||||||
|
manualTrigger = !!event.isTrigger;
|
||||||
|
|
||||||
|
if ( !opts.autoTrigger && !manualTrigger ) {
|
||||||
|
// ignore browser's `contextmenu` events
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Prevent browser from opening the system context menu
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.currentTarget = event.target;
|
||||||
|
this.extraData = event._extraData || {};
|
||||||
|
|
||||||
|
ui = { menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData,
|
||||||
|
originalEvent: event, result: null };
|
||||||
|
|
||||||
|
if ( !recursive ) {
|
||||||
|
res = this._trigger("beforeOpen", event, ui);
|
||||||
|
promise = (ui.result && $.isFunction(ui.result.promise)) ? ui.result : null;
|
||||||
|
ui.result = null;
|
||||||
|
if ( res === false ) {
|
||||||
|
this.currentTarget = null;
|
||||||
|
return false;
|
||||||
|
} else if ( promise ) {
|
||||||
|
// Handler returned a Deferred or Promise. Delay menu open until
|
||||||
|
// the promise is resolved
|
||||||
|
promise.done(function() {
|
||||||
|
self._openMenu(event, true);
|
||||||
|
});
|
||||||
|
this.currentTarget = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ui.menu = this.$menu; // Might have changed in beforeOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register global event handlers that close the dropdown-menu
|
||||||
|
$(document).on("keydown" + this.eventNamespace, function(event) {
|
||||||
|
if ( event.which === $.ui.keyCode.ESCAPE ) {
|
||||||
|
self._closeMenu();
|
||||||
|
}
|
||||||
|
}).on("mousedown" + this.eventNamespace + " touchstart" + this.eventNamespace,
|
||||||
|
function(event) {
|
||||||
|
// Close menu when clicked outside menu
|
||||||
|
if ( !$(event.target).closest(".ui-menu-item").length ) {
|
||||||
|
self._closeMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$(window).on("blur" + this.eventNamespace, function(event) {
|
||||||
|
if ( opts.closeOnWindowBlur ) {
|
||||||
|
self._closeMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// required for custom positioning (issue #18 and #13).
|
||||||
|
if ($.isFunction(posOption)) {
|
||||||
|
posOption = posOption(event, ui);
|
||||||
|
}
|
||||||
|
posOption = $.extend({
|
||||||
|
my: "left top",
|
||||||
|
at: "left bottom",
|
||||||
|
// if called by 'open' method, event does not have pageX/Y
|
||||||
|
of: (event.pageX === undefined) ? event.target : event,
|
||||||
|
collision: "fit"
|
||||||
|
}, posOption);
|
||||||
|
|
||||||
|
// Update entry statuses from callbacks
|
||||||
|
this._updateEntries(this.$menu);
|
||||||
|
|
||||||
|
// Finally display the popup
|
||||||
|
this.$menu
|
||||||
|
.show() // required to fix positioning error
|
||||||
|
.css({
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0
|
||||||
|
}).position(posOption)
|
||||||
|
.hide(); // hide again, so we can apply nice effects
|
||||||
|
|
||||||
|
if ( opts.preventContextMenuForPopup ) {
|
||||||
|
this.$menu.on("contextmenu" + this.eventNamespace, function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this._show(this.$menu, opts.show, function() {
|
||||||
|
var $first;
|
||||||
|
|
||||||
|
// Set focus to first active menu entry
|
||||||
|
if ( opts.autoFocus ) {
|
||||||
|
self.previousFocus = $(event.target);
|
||||||
|
// self.$menu.focus();
|
||||||
|
$first = self.$menu
|
||||||
|
.children("li.ui-menu-item")
|
||||||
|
.not(".ui-state-disabled")
|
||||||
|
.first();
|
||||||
|
self.$menu.menu("focus", null, $first).focus();
|
||||||
|
}
|
||||||
|
self._trigger.call(self, "open", event, ui);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** Close popup. */
|
||||||
|
_closeMenu: function(immediately) {
|
||||||
|
var self = this,
|
||||||
|
hideOpts = immediately ? false : this.options.hide,
|
||||||
|
ui = { menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData };
|
||||||
|
|
||||||
|
// Note: we don't want to unbind the 'contextmenu' event
|
||||||
|
$(document)
|
||||||
|
.off("mousedown" + this.eventNamespace)
|
||||||
|
.off("touchstart" + this.eventNamespace)
|
||||||
|
.off("keydown" + this.eventNamespace);
|
||||||
|
$(window)
|
||||||
|
.off("blur" + this.eventNamespace);
|
||||||
|
|
||||||
|
self.currentTarget = null; // issue #44 after hide animation is too late
|
||||||
|
self.extraData = {};
|
||||||
|
if ( this.$menu ) { // #88: widget might have been destroyed already
|
||||||
|
this.$menu
|
||||||
|
.off("contextmenu" + this.eventNamespace);
|
||||||
|
this._hide(this.$menu, hideOpts, function() {
|
||||||
|
if ( self.previousFocus ) {
|
||||||
|
self.previousFocus.focus();
|
||||||
|
self.previousFocus = null;
|
||||||
|
}
|
||||||
|
self._trigger("close", null, ui);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self._trigger("close", null, ui);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** Handle $().contextmenu("option", key, value) calls. */
|
||||||
|
_setOption: function(key, value) {
|
||||||
|
switch (key) {
|
||||||
|
case "menu":
|
||||||
|
this.replaceMenu(value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$.Widget.prototype._setOption.apply(this, arguments);
|
||||||
|
},
|
||||||
|
/** Return ui-menu entry (<LI> tag). */
|
||||||
|
_getMenuEntry: function(cmd) {
|
||||||
|
return this.$menu.find("li[data-command=" + cmd + "]");
|
||||||
|
},
|
||||||
|
/** Close context menu. */
|
||||||
|
close: function() {
|
||||||
|
if (this.isOpen()) {
|
||||||
|
this._closeMenu();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/* Apply status callbacks when menu is opened. */
|
||||||
|
_updateEntries: function() {
|
||||||
|
var self = this,
|
||||||
|
ui = {
|
||||||
|
menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData };
|
||||||
|
|
||||||
|
$.each(this.$menu.find(".ui-menu-item"), function(i, o) {
|
||||||
|
var $entry = $(o),
|
||||||
|
fn = $entry.data("disabledHandler"),
|
||||||
|
res = fn ? fn({ type: "disabled" }, ui) : null;
|
||||||
|
|
||||||
|
ui.item = $entry;
|
||||||
|
ui.cmd = $entry.attr("data-command");
|
||||||
|
// Evaluate `disabled()` callback
|
||||||
|
if ( res != null ) {
|
||||||
|
self.enableEntry(ui.cmd, !res);
|
||||||
|
self.showEntry(ui.cmd, res !== "hide");
|
||||||
|
}
|
||||||
|
// Evaluate `title()` callback
|
||||||
|
fn = $entry.data("titleHandler"),
|
||||||
|
res = fn ? fn({ type: "title" }, ui) : null;
|
||||||
|
if ( res != null ) {
|
||||||
|
self.setTitle(ui.cmd, "" + res);
|
||||||
|
}
|
||||||
|
// Evaluate `tooltip()` callback
|
||||||
|
fn = $entry.data("tooltipHandler"),
|
||||||
|
res = fn ? fn({ type: "tooltip" }, ui) : null;
|
||||||
|
if ( res != null ) {
|
||||||
|
$entry.attr("title", "" + res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** Enable or disable the menu command. */
|
||||||
|
enableEntry: function(cmd, flag) {
|
||||||
|
this._getMenuEntry(cmd).toggleClass("ui-state-disabled", (flag === false));
|
||||||
|
},
|
||||||
|
/** Return ui-menu entry (LI tag) as jQuery object. */
|
||||||
|
getEntry: function(cmd) {
|
||||||
|
return this._getMenuEntry(cmd);
|
||||||
|
},
|
||||||
|
/** Return ui-menu entry wrapper as jQuery object.
|
||||||
|
UI 1.10: this is the <a> tag inside the LI
|
||||||
|
UI 1.11: this is the LI istself
|
||||||
|
UI 1.12: this is the <div> tag inside the LI
|
||||||
|
*/
|
||||||
|
getEntryWrapper: function(cmd) {
|
||||||
|
return this._getMenuEntry(cmd).find(">[role=menuitem]").addBack("[role=menuitem]");
|
||||||
|
},
|
||||||
|
/** Return Menu element (UL). */
|
||||||
|
getMenu: function() {
|
||||||
|
return this.$menu;
|
||||||
|
},
|
||||||
|
/** Return true if menu is open. */
|
||||||
|
isOpen: function() {
|
||||||
|
// return this.$menu && this.$menu.is(":visible");
|
||||||
|
return !!this.$menu && !!this.currentTarget;
|
||||||
|
},
|
||||||
|
/** Open context menu on a specific target (must match options.delegate)
|
||||||
|
* Optional `extraData` is passed to event handlers as `ui.extraData`.
|
||||||
|
*/
|
||||||
|
open: function(targetOrEvent, extraData) {
|
||||||
|
// Fake a 'contextmenu' event
|
||||||
|
extraData = extraData || {};
|
||||||
|
|
||||||
|
var isEvent = (targetOrEvent && targetOrEvent.type && targetOrEvent.target),
|
||||||
|
event = isEvent ? targetOrEvent : {},
|
||||||
|
target = isEvent ? targetOrEvent.target : targetOrEvent,
|
||||||
|
e = jQuery.Event("contextmenu", {
|
||||||
|
target: $(target).get(0),
|
||||||
|
pageX: event.pageX,
|
||||||
|
pageY: event.pageY,
|
||||||
|
originalEvent: isEvent ? targetOrEvent : undefined,
|
||||||
|
_extraData: extraData
|
||||||
|
});
|
||||||
|
return this.element.trigger(e);
|
||||||
|
},
|
||||||
|
/** Replace the menu altogether. */
|
||||||
|
replaceMenu: function(data) {
|
||||||
|
this._createUiMenu(data);
|
||||||
|
},
|
||||||
|
/** Redefine a whole menu entry. */
|
||||||
|
setEntry: function(cmd, entry) {
|
||||||
|
var $ul,
|
||||||
|
$entryLi = this._getMenuEntry(cmd);
|
||||||
|
|
||||||
|
if (typeof entry === "string") {
|
||||||
|
window.console && window.console.warn(
|
||||||
|
"setEntry(cmd, t) with a plain string title is deprecated since v1.18." +
|
||||||
|
"Use setTitle(cmd, '" + entry + "') instead.");
|
||||||
|
return this.setTitle(cmd, entry);
|
||||||
|
}
|
||||||
|
$entryLi.empty();
|
||||||
|
entry.cmd = entry.cmd || cmd;
|
||||||
|
$.moogle.contextmenu.createEntryMarkup(entry, $entryLi);
|
||||||
|
if ($.isArray(entry.children)) {
|
||||||
|
$ul = $("<ul/>").appendTo($entryLi);
|
||||||
|
$.moogle.contextmenu.createMenuMarkup(entry.children, $ul);
|
||||||
|
}
|
||||||
|
// #110: jQuery UI 1.12: refresh only works when this class is not set:
|
||||||
|
$entryLi.removeClass("ui-menu-item");
|
||||||
|
this.getMenu().menu("refresh");
|
||||||
|
},
|
||||||
|
/** Set icon (pass null to remove). */
|
||||||
|
setIcon: function(cmd, icon) {
|
||||||
|
return this.updateEntry(cmd, { uiIcon: icon });
|
||||||
|
},
|
||||||
|
/** Set title. */
|
||||||
|
setTitle: function(cmd, title) {
|
||||||
|
return this.updateEntry(cmd, { title: title });
|
||||||
|
},
|
||||||
|
// /** Set tooltip (pass null to remove). */
|
||||||
|
// setTooltip: function(cmd, tooltip) {
|
||||||
|
// this._getMenuEntry(cmd).attr("title", tooltip);
|
||||||
|
// },
|
||||||
|
/** Show or hide the menu command. */
|
||||||
|
showEntry: function(cmd, flag) {
|
||||||
|
this._getMenuEntry(cmd).toggle(flag !== false);
|
||||||
|
},
|
||||||
|
/** Redefine selective attributes of a menu entry. */
|
||||||
|
updateEntry: function(cmd, entry) {
|
||||||
|
var $icon, $wrapper,
|
||||||
|
$entryLi = this._getMenuEntry(cmd);
|
||||||
|
|
||||||
|
if ( entry.title !== undefined ) {
|
||||||
|
$.moogle.contextmenu.updateTitle($entryLi, "" + entry.title);
|
||||||
|
}
|
||||||
|
if ( entry.tooltip !== undefined ) {
|
||||||
|
if ( entry.tooltip === null ) {
|
||||||
|
$entryLi.removeAttr("title");
|
||||||
|
} else {
|
||||||
|
$entryLi.attr("title", entry.tooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( entry.uiIcon !== undefined ) {
|
||||||
|
$wrapper = this.getEntryWrapper(cmd),
|
||||||
|
$icon = $wrapper.find("span.ui-icon").not(".ui-menu-icon");
|
||||||
|
$icon.remove();
|
||||||
|
if ( entry.uiIcon ) {
|
||||||
|
$wrapper.append($("<span class='ui-icon' />").addClass(entry.uiIcon));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( entry.hide !== undefined ) {
|
||||||
|
$entryLi.toggle(!entry.hide);
|
||||||
|
} else if ( entry.show !== undefined ) {
|
||||||
|
// Note: `show` is an undocumented variant. `hide: false` is preferred
|
||||||
|
$entryLi.toggle(!!entry.show);
|
||||||
|
}
|
||||||
|
// if ( entry.isHeader !== undefined ) {
|
||||||
|
// $entryLi.toggleClass("ui-widget-header", !!entry.isHeader);
|
||||||
|
// }
|
||||||
|
if ( entry.data !== undefined ) {
|
||||||
|
$entryLi.data(entry.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set/clear class names, but handle ui-state-disabled separately
|
||||||
|
if ( entry.disabled === undefined ) {
|
||||||
|
entry.disabled = $entryLi.hasClass("ui-state-disabled");
|
||||||
|
}
|
||||||
|
if ( entry.setClass ) {
|
||||||
|
if ( $entryLi.hasClass("ui-menu-item") ) {
|
||||||
|
entry.setClass += " ui-menu-item";
|
||||||
|
}
|
||||||
|
$entryLi.removeClass();
|
||||||
|
$entryLi.addClass(entry.setClass);
|
||||||
|
} else if ( entry.addClass ) {
|
||||||
|
$entryLi.addClass(entry.addClass);
|
||||||
|
}
|
||||||
|
$entryLi.toggleClass("ui-state-disabled", !!entry.disabled);
|
||||||
|
// // #110: jQuery UI 1.12: refresh only works when this class is not set:
|
||||||
|
// $entryLi.removeClass("ui-menu-item");
|
||||||
|
// this.getMenu().menu("refresh");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Global functions
|
||||||
|
*/
|
||||||
|
$.extend($.moogle.contextmenu, {
|
||||||
|
/** Convert a menu description into a into a <li> content. */
|
||||||
|
createEntryMarkup: function(entry, $parentLi) {
|
||||||
|
var $wrapper = null;
|
||||||
|
|
||||||
|
$parentLi.attr("data-command", entry.cmd);
|
||||||
|
|
||||||
|
if ( !/[^\-\u2014\u2013\s]/.test( entry.title ) ) {
|
||||||
|
// hyphen, em dash, en dash: separator as defined by UI Menu 1.10
|
||||||
|
$parentLi.text(entry.title);
|
||||||
|
} else {
|
||||||
|
if ( isLTE110 ) {
|
||||||
|
// jQuery UI Menu 1.10 or before required an `<a>` tag
|
||||||
|
$wrapper = $("<a/>", {
|
||||||
|
html: "" + entry.title,
|
||||||
|
href: "#"
|
||||||
|
}).appendTo($parentLi);
|
||||||
|
|
||||||
|
} else if ( isLTE111 ) {
|
||||||
|
// jQuery UI Menu 1.11 preferes to avoid `<a>` tags or <div> wrapper
|
||||||
|
$parentLi.html("" + entry.title);
|
||||||
|
$wrapper = $parentLi;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// jQuery UI Menu 1.12 introduced `<div>` wrappers
|
||||||
|
$wrapper = $("<div/>", {
|
||||||
|
html: "" + entry.title
|
||||||
|
}).appendTo($parentLi);
|
||||||
|
}
|
||||||
|
if ( entry.uiIcon ) {
|
||||||
|
$wrapper.append($("<span class='ui-icon' />").addClass(entry.uiIcon));
|
||||||
|
}
|
||||||
|
// Store option callbacks in entry's data
|
||||||
|
$.each( [ "action", "disabled", "title", "tooltip" ], function(i, attr) {
|
||||||
|
if ( $.isFunction(entry[attr]) ) {
|
||||||
|
$parentLi.data(attr + "Handler", entry[attr]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if ( entry.disabled === true ) {
|
||||||
|
$parentLi.addClass("ui-state-disabled");
|
||||||
|
}
|
||||||
|
if ( entry.isHeader ) {
|
||||||
|
$parentLi.addClass("ui-widget-header");
|
||||||
|
}
|
||||||
|
if ( entry.addClass ) {
|
||||||
|
$parentLi.addClass(entry.addClass);
|
||||||
|
}
|
||||||
|
if ( $.isPlainObject(entry.data) ) {
|
||||||
|
$parentLi.data(entry.data);
|
||||||
|
}
|
||||||
|
if ( typeof entry.tooltip === "string" ) {
|
||||||
|
$parentLi.attr("title", entry.tooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** Convert a nested array of command objects into a <ul> structure. */
|
||||||
|
createMenuMarkup: function(options, $parentUl) {
|
||||||
|
var i, menu, $ul, $li;
|
||||||
|
if ( $parentUl == null ) {
|
||||||
|
$parentUl = $("<ul class='ui-helper-hidden' />").appendTo("body");
|
||||||
|
}
|
||||||
|
for (i = 0; i < options.length; i++) {
|
||||||
|
menu = options[i];
|
||||||
|
$li = $("<li/>").appendTo($parentUl);
|
||||||
|
|
||||||
|
$.moogle.contextmenu.createEntryMarkup(menu, $li);
|
||||||
|
|
||||||
|
if ( $.isArray(menu.children) ) {
|
||||||
|
$ul = $("<ul/>").appendTo($li);
|
||||||
|
$.moogle.contextmenu.createMenuMarkup(menu.children, $ul);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $parentUl;
|
||||||
|
},
|
||||||
|
/** Returns true if the menu item has child menu items */
|
||||||
|
isMenu: function(item) {
|
||||||
|
if ( isLTE110 ) {
|
||||||
|
return item.has(">a[aria-haspopup='true']").length > 0;
|
||||||
|
} else if ( isLTE111 ) { // jQuery UI 1.11 used no tag wrappers
|
||||||
|
return item.is("[aria-haspopup='true']");
|
||||||
|
} else {
|
||||||
|
return item.has(">div[aria-haspopup='true']").length > 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** Replace the title of elem', but retain icons andchild entries. */
|
||||||
|
replaceFirstTextNodeChild: function(elem, html) {
|
||||||
|
var $icons = elem.find(">span.ui-icon,>ul.ui-menu").detach();
|
||||||
|
|
||||||
|
elem
|
||||||
|
.empty()
|
||||||
|
.html(html)
|
||||||
|
.append($icons);
|
||||||
|
},
|
||||||
|
/** Updates the menu item's title */
|
||||||
|
updateTitle: function(item, title) {
|
||||||
|
if ( isLTE110 ) { // jQuery UI 1.10 and before used <a> tags
|
||||||
|
$.moogle.contextmenu.replaceFirstTextNodeChild($("a", item), title);
|
||||||
|
} else if ( isLTE111 ) { // jQuery UI 1.11 used no tag wrappers
|
||||||
|
$.moogle.contextmenu.replaceFirstTextNodeChild(item, title);
|
||||||
|
} else { // jQuery UI 1.12+ introduced <div> tag wrappers
|
||||||
|
$.moogle.contextmenu.replaceFirstTextNodeChild($("div", item), title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}));
|
||||||
1
public/libraries/jquery.ui-contextmenu.min.js.map
Normal file
1
public/libraries/jquery.ui-contextmenu.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -74,6 +74,12 @@ span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-tit
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
.ui-autocomplete {
|
.ui-autocomplete {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -168,18 +174,29 @@ div.ui-tooltip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Allow to use <kbd> elements inside the title to define shortcut hints. */
|
/* Allow to use <kbd> elements inside the title to define shortcut hints. */
|
||||||
.ui-menu kbd {
|
.ui-menu kbd, button kbd {
|
||||||
margin-left: 30px;
|
|
||||||
float: right;
|
|
||||||
color: black;
|
color: black;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#loader-wrapper{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1000;background-color:#fff;opacity:1;transition:opacity 2s ease}
|
.ui-menu kbd {
|
||||||
#loader{display:block;position:relative;left:50%;top:50%;width:150px;height:150px;margin:-75px 0 0 -75px;border-radius:50%;border:3px solid transparent;border-top-color:#777;-webkit-animation:spin 2s linear infinite;animation:spin 2s linear infinite}
|
margin-left: 30px;
|
||||||
#loader:before{content:"";position:absolute;top:5px;left:5px;right:5px;bottom:5px;border-radius:50%;border:3px solid transparent;border-top-color:#aaa;-webkit-animation:spin 3s linear infinite;animation:spin 3s linear infinite}
|
float: right;
|
||||||
#loader:after{content:"";position:absolute;top:15px;left:15px;right:15px;bottom:15px;border-radius:50%;border:3px solid transparent;border-top-color:#ddd;-webkit-animation:spin 1.5s linear infinite;animation:spin 1.5s linear infinite}
|
}
|
||||||
@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg)}}
|
|
||||||
@keyframes spin{0%{-webkit-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg)}}
|
#note-id-display {
|
||||||
|
color: lightgrey;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#note-source {
|
||||||
|
height: 98%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suppressed {
|
||||||
|
filter: opacity(7%);
|
||||||
|
}
|
||||||
@@ -4,11 +4,12 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const anonymization = require('../../services/anonymization');
|
const anonymization = require('../../services/anonymization');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.post('/anonymize', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/anonymize', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await anonymization.anonymize();
|
await anonymization.anonymize();
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -4,9 +4,10 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const app_info = require('../../services/app_info');
|
const app_info = require('../../services/app_info');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('', auth.checkApiAuth, async (req, res, next) => {
|
router.get('', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
res.send(app_info);
|
res.send(app_info);
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -7,8 +7,9 @@ const utils = require('../../services/utils');
|
|||||||
const sync_table = require('../../services/sync_table');
|
const sync_table = require('../../services/sync_table');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
const log = require('../../services/log');
|
const log = require('../../services/log');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.post('/cleanup-soft-deleted-items', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/cleanup-soft-deleted-items', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
const noteIdsToDelete = await sql.getFirstColumn("SELECT note_id FROM notes WHERE is_deleted = 1");
|
const noteIdsToDelete = await sql.getFirstColumn("SELECT note_id FROM notes WHERE is_deleted = 1");
|
||||||
const noteIdsSql = noteIdsToDelete
|
const noteIdsSql = noteIdsToDelete
|
||||||
@@ -34,14 +35,14 @@ router.post('/cleanup-soft-deleted-items', auth.checkApiAuth, async (req, res, n
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.post('/vacuum-database', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/vacuum-database', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await sql.execute("VACUUM");
|
await sql.execute("VACUUM");
|
||||||
|
|
||||||
log.info("Database has been vacuumed.");
|
log.info("Database has been vacuumed.");
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -4,14 +4,15 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('', auth.checkApiAuth, async (req, res, next) => {
|
router.get('', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await deleteOld();
|
await deleteOld();
|
||||||
|
|
||||||
const result = await sql.getAll("SELECT * FROM event_log ORDER BY date_added DESC");
|
const result = await sql.getAll("SELECT * FROM event_log ORDER BY date_added DESC");
|
||||||
|
|
||||||
res.send(result);
|
res.send(result);
|
||||||
});
|
}));
|
||||||
|
|
||||||
async function deleteOld() {
|
async function deleteOld() {
|
||||||
const cutoffId = await sql.getFirstValue("SELECT id FROM event_log ORDER BY id DESC LIMIT 1000, 1");
|
const cutoffId = await sql.getFirstValue("SELECT id FROM event_log ORDER BY id DESC LIMIT 1000, 1");
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ const sql = require('../../services/sql');
|
|||||||
const data_dir = require('../../services/data_dir');
|
const data_dir = require('../../services/data_dir');
|
||||||
const html = require('html');
|
const html = require('html');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('/:noteId/to/:directory', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/:noteId/to/:directory', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
|
const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ router.get('/:noteId/to/:directory', auth.checkApiAuth, async (req, res, next) =
|
|||||||
await exportNote(noteTreeId, completeExportDir);
|
await exportNote(noteTreeId, completeExportDir);
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
async function exportNote(noteTreeId, dir) {
|
async function exportNote(noteTreeId, dir) {
|
||||||
const noteTree = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
const noteTree = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
||||||
|
|||||||
142
routes/api/image.js
Normal file
142
routes/api/image.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const sql = require('../../services/sql');
|
||||||
|
const auth = require('../../services/auth');
|
||||||
|
const utils = require('../../services/utils');
|
||||||
|
const sync_table = require('../../services/sync_table');
|
||||||
|
const multer = require('multer')();
|
||||||
|
const imagemin = require('imagemin');
|
||||||
|
const imageminMozJpeg = require('imagemin-mozjpeg');
|
||||||
|
const imageminPngQuant = require('imagemin-pngquant');
|
||||||
|
const imageminGifLossy = require('imagemin-giflossy');
|
||||||
|
const jimp = require('jimp');
|
||||||
|
const imageType = require('image-type');
|
||||||
|
const sanitizeFilename = require('sanitize-filename');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
|
router.get('/:imageId/:filename', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => {
|
||||||
|
const image = await sql.getFirst("SELECT * FROM images WHERE image_id = ?", [req.params.imageId]);
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
return res.status(404).send({});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.set('Content-Type', 'image/' + image.format);
|
||||||
|
|
||||||
|
res.send(image.data);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async (req, res, next) => {
|
||||||
|
const sourceId = req.headers.source_id;
|
||||||
|
const noteId = req.query.noteId;
|
||||||
|
const file = req.file;
|
||||||
|
|
||||||
|
const note = await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [noteId]);
|
||||||
|
|
||||||
|
if (!note) {
|
||||||
|
return res.status(404).send(`Note ${noteId} doesn't exist.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!["image/png", "image/jpeg", "image/gif"].includes(file.mimetype)) {
|
||||||
|
return res.status(400).send("Unknown image type: " + file.mimetype);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = utils.nowDate();
|
||||||
|
|
||||||
|
const resizedImage = await resize(file.buffer);
|
||||||
|
const optimizedImage = await optimize(resizedImage);
|
||||||
|
|
||||||
|
const imageFormat = imageType(optimizedImage);
|
||||||
|
|
||||||
|
const fileNameWithouExtension = file.originalname.replace(/\.[^/.]+$/, "");
|
||||||
|
const fileName = sanitizeFilename(fileNameWithouExtension + "." + imageFormat.ext);
|
||||||
|
|
||||||
|
const imageId = utils.newImageId();
|
||||||
|
|
||||||
|
await sql.doInTransaction(async () => {
|
||||||
|
await sql.insert("images", {
|
||||||
|
image_id: imageId,
|
||||||
|
format: imageFormat.ext,
|
||||||
|
name: fileName,
|
||||||
|
checksum: utils.hash(optimizedImage),
|
||||||
|
data: optimizedImage,
|
||||||
|
is_deleted: 0,
|
||||||
|
date_modified: now,
|
||||||
|
date_created: now
|
||||||
|
});
|
||||||
|
|
||||||
|
await sync_table.addImageSync(imageId, sourceId);
|
||||||
|
|
||||||
|
const noteImageId = utils.newNoteImageId();
|
||||||
|
|
||||||
|
await sql.insert("notes_image", {
|
||||||
|
note_image_id: noteImageId,
|
||||||
|
note_id: noteId,
|
||||||
|
image_id: imageId,
|
||||||
|
is_deleted: 0,
|
||||||
|
date_modified: now,
|
||||||
|
date_created: now
|
||||||
|
});
|
||||||
|
|
||||||
|
await sync_table.addNoteImageSync(noteImageId, sourceId);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
uploaded: true,
|
||||||
|
url: `/api/images/${imageId}/${fileName}`
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
const MAX_SIZE = 1000;
|
||||||
|
const MAX_BYTE_SIZE = 200000; // images should have under 100 KBs
|
||||||
|
|
||||||
|
async function resize(buffer) {
|
||||||
|
const image = await jimp.read(buffer);
|
||||||
|
|
||||||
|
if (image.bitmap.width > image.bitmap.height && image.bitmap.width > MAX_SIZE) {
|
||||||
|
image.resize(MAX_SIZE, jimp.AUTO);
|
||||||
|
}
|
||||||
|
else if (image.bitmap.height > MAX_SIZE) {
|
||||||
|
image.resize(jimp.AUTO, MAX_SIZE);
|
||||||
|
}
|
||||||
|
else if (buffer.byteLength <= MAX_BYTE_SIZE) {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we do resizing with max quality which will be trimmed during optimization step next
|
||||||
|
image.quality(100);
|
||||||
|
|
||||||
|
// when converting PNG to JPG we lose alpha channel, this is replaced by white to match Trilium white background
|
||||||
|
image.background(0xFFFFFFFF);
|
||||||
|
|
||||||
|
// getBuffer doesn't support promises so this workaround
|
||||||
|
return await new Promise((resolve, reject) => image.getBuffer(jimp.MIME_JPEG, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function optimize(buffer) {
|
||||||
|
return await imagemin.buffer(buffer, {
|
||||||
|
plugins: [
|
||||||
|
imageminMozJpeg({
|
||||||
|
quality: 50
|
||||||
|
}),
|
||||||
|
imageminPngQuant({
|
||||||
|
quality: "0-70"
|
||||||
|
}),
|
||||||
|
imageminGifLossy({
|
||||||
|
lossy: 80,
|
||||||
|
optimize: '3' // needs to be string
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -8,8 +8,9 @@ const data_dir = require('../../services/data_dir');
|
|||||||
const utils = require('../../services/utils');
|
const utils = require('../../services/utils');
|
||||||
const sync_table = require('../../services/sync_table');
|
const sync_table = require('../../services/sync_table');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('/:directory/to/:parentNoteId', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/:directory/to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
|
const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
|
||||||
const parentNoteId = req.params.parentNoteId;
|
const parentNoteId = req.params.parentNoteId;
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ router.get('/:directory/to/:parentNoteId', auth.checkApiAuth, async (req, res, n
|
|||||||
await sql.doInTransaction(async () => await importNotes(dir, parentNoteId));
|
await sql.doInTransaction(async () => await importNotes(dir, parentNoteId));
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
async function importNotes(dir, parentNoteId) {
|
async function importNotes(dir, parentNoteId) {
|
||||||
const parent = await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [parentNoteId]);
|
const parent = await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [parentNoteId]);
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ const auth = require('../../services/auth');
|
|||||||
const password_encryption = require('../../services/password_encryption');
|
const password_encryption = require('../../services/password_encryption');
|
||||||
const protected_session = require('../../services/protected_session');
|
const protected_session = require('../../services/protected_session');
|
||||||
const app_info = require('../../services/app_info');
|
const app_info = require('../../services/app_info');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.post('/sync', async (req, res, next) => {
|
router.post('/sync', wrap(async (req, res, next) => {
|
||||||
const timestampStr = req.body.timestamp;
|
const timestampStr = req.body.timestamp;
|
||||||
|
|
||||||
const timestamp = utils.parseDate(timestampStr);
|
const timestamp = utils.parseDate(timestampStr);
|
||||||
@@ -44,10 +45,10 @@ router.post('/sync', async (req, res, next) => {
|
|||||||
res.send({
|
res.send({
|
||||||
sourceId: source_id.getCurrentSourceId()
|
sourceId: source_id.getCurrentSourceId()
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
// this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
|
// this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
|
||||||
router.post('/protected', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/protected', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const password = req.body.password;
|
const password = req.body.password;
|
||||||
|
|
||||||
if (!await password_encryption.verifyPassword(password)) {
|
if (!await password_encryption.verifyPassword(password)) {
|
||||||
@@ -67,6 +68,6 @@ router.post('/protected', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
success: true,
|
success: true,
|
||||||
protectedSessionId: protectedSessionId
|
protectedSessionId: protectedSessionId
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -6,20 +6,21 @@ const auth = require('../../services/auth');
|
|||||||
const options = require('../../services/options');
|
const options = require('../../services/options');
|
||||||
const migration = require('../../services/migration');
|
const migration = require('../../services/migration');
|
||||||
const app_info = require('../../services/app_info');
|
const app_info = require('../../services/app_info');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('', auth.checkApiAuthForMigrationPage, async (req, res, next) => {
|
router.get('', auth.checkApiAuthForMigrationPage, wrap(async (req, res, next) => {
|
||||||
res.send({
|
res.send({
|
||||||
db_version: parseInt(await options.getOption('db_version')),
|
db_version: parseInt(await options.getOption('db_version')),
|
||||||
app_db_version: app_info.db_version
|
app_db_version: app_info.db_version
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.post('', auth.checkApiAuthForMigrationPage, async (req, res, next) => {
|
router.post('', auth.checkApiAuthForMigrationPage, wrap(async (req, res, next) => {
|
||||||
const migrations = await migration.migrate();
|
const migrations = await migration.migrate();
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
migrations: migrations
|
migrations: migrations
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -7,8 +7,9 @@ const auth = require('../../services/auth');
|
|||||||
const data_encryption = require('../../services/data_encryption');
|
const data_encryption = require('../../services/data_encryption');
|
||||||
const protected_session = require('../../services/protected_session');
|
const protected_session = require('../../services/protected_session');
|
||||||
const sync_table = require('../../services/sync_table');
|
const sync_table = require('../../services/sync_table');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('/:noteId', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
const history = await sql.getAll("SELECT * FROM notes_history WHERE note_id = ? order by date_modified_to desc", [noteId]);
|
const history = await sql.getAll("SELECT * FROM notes_history WHERE note_id = ? order by date_modified_to desc", [noteId]);
|
||||||
|
|
||||||
@@ -22,9 +23,9 @@ router.get('/:noteId', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.send(history);
|
res.send(history);
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('', auth.checkApiAuth, async (req, res, next) => {
|
router.put('', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
@@ -34,6 +35,6 @@ router.put('', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.send();
|
res.send();
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -8,8 +8,9 @@ const notes = require('../../services/notes');
|
|||||||
const log = require('../../services/log');
|
const log = require('../../services/log');
|
||||||
const protected_session = require('../../services/protected_session');
|
const protected_session = require('../../services/protected_session');
|
||||||
const data_encryption = require('../../services/data_encryption');
|
const data_encryption = require('../../services/data_encryption');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('/:noteId', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
|
|
||||||
const detail = await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [noteId]);
|
const detail = await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [noteId]);
|
||||||
@@ -30,9 +31,9 @@ router.get('/:noteId', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
res.send({
|
res.send({
|
||||||
detail: detail
|
detail: detail
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.post('/:parentNoteId/children', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/:parentNoteId/children', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
const parentNoteId = req.params.parentNoteId;
|
const parentNoteId = req.params.parentNoteId;
|
||||||
const note = req.body;
|
const note = req.body;
|
||||||
@@ -43,9 +44,9 @@ router.post('/:parentNoteId/children', auth.checkApiAuth, async (req, res, next)
|
|||||||
'note_id': noteId,
|
'note_id': noteId,
|
||||||
'note_tree_id': noteTreeId
|
'note_tree_id': noteTreeId
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/:noteId', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const note = req.body;
|
const note = req.body;
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
@@ -54,20 +55,20 @@ router.put('/:noteId', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
await notes.updateNote(noteId, note, dataKey, sourceId);
|
await notes.updateNote(noteId, note, dataKey, sourceId);
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.delete('/:noteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
router.delete('/:noteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
await notes.deleteNote(req.params.noteTreeId, req.headers.source_id);
|
await notes.deleteNote(req.params.noteTreeId, req.headers.source_id);
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const search = '%' + req.query.search + '%';
|
const search = '%' + req.query.search + '%';
|
||||||
|
|
||||||
const result = await sql.getAll("SELECT note_id FROM notes WHERE note_title liKE ? OR note_text LIKE ?", [search, search]);
|
const result = await sql.getAll("SELECT note_id FROM notes WHERE note_title LIKE ? OR note_text LIKE ?", [search, search]);
|
||||||
|
|
||||||
const noteIdList = [];
|
const noteIdList = [];
|
||||||
|
|
||||||
@@ -76,6 +77,6 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.send(noteIdList);
|
res.send(noteIdList);
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -3,15 +3,27 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
const utils = require('../../services/utils');
|
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const utils = require('../../services/utils');
|
||||||
const sync_table = require('../../services/sync_table');
|
const sync_table = require('../../services/sync_table');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, async (req, res, next) => {
|
/**
|
||||||
|
* Code in this file deals with moving and cloning note tree rows. Relationship between note and parent note is unique
|
||||||
|
* for not deleted note trees. There may be multiple deleted note-parent note relationships.
|
||||||
|
*/
|
||||||
|
|
||||||
|
router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeId = req.params.noteTreeId;
|
const noteTreeId = req.params.noteTreeId;
|
||||||
const parentNoteId = req.params.parentNoteId;
|
const parentNoteId = req.params.parentNoteId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
|
const noteToMove = await getNoteTree(noteTreeId);
|
||||||
|
|
||||||
|
if (!await validateParentChild(res, parentNoteId, noteToMove.note_id, noteTreeId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const maxNotePos = await sql.getFirstValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]);
|
const maxNotePos = await sql.getFirstValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]);
|
||||||
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 1;
|
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 1;
|
||||||
|
|
||||||
@@ -24,89 +36,77 @@ router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, async (req,
|
|||||||
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send({});
|
res.send({ success: true });
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeId = req.params.noteTreeId;
|
const noteTreeId = req.params.noteTreeId;
|
||||||
const beforeNoteTreeId = req.params.beforeNoteTreeId;
|
const beforeNoteTreeId = req.params.beforeNoteTreeId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
const beforeNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [beforeNoteTreeId]);
|
const noteToMove = await getNoteTree(noteTreeId);
|
||||||
|
const beforeNote = await getNoteTree(beforeNoteTreeId);
|
||||||
|
|
||||||
if (beforeNote) {
|
if (!await validateParentChild(res, beforeNote.parent_note_id, noteToMove.note_id, noteTreeId)) {
|
||||||
await sql.doInTransaction(async () => {
|
return;
|
||||||
// we don't change date_modified so other changes are prioritized in case of conflict
|
|
||||||
// also we would have to sync all those modified note trees otherwise hash checks would fail
|
|
||||||
await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position >= ? AND is_deleted = 0",
|
|
||||||
[beforeNote.parent_note_id, beforeNote.note_position]);
|
|
||||||
|
|
||||||
await sync_table.addNoteReorderingSync(beforeNote.parent_note_id, sourceId);
|
|
||||||
|
|
||||||
const now = utils.nowDate();
|
|
||||||
|
|
||||||
await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?",
|
|
||||||
[beforeNote.parent_note_id, beforeNote.note_position, now, noteTreeId]);
|
|
||||||
|
|
||||||
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
|
||||||
});
|
|
||||||
|
|
||||||
res.send({});
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
res.status(500).send("Before note " + beforeNoteTreeId + " doesn't exist.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
await sql.doInTransaction(async () => {
|
||||||
|
// we don't change date_modified so other changes are prioritized in case of conflict
|
||||||
|
// also we would have to sync all those modified note trees otherwise hash checks would fail
|
||||||
|
await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position >= ? AND is_deleted = 0",
|
||||||
|
[beforeNote.parent_note_id, beforeNote.note_position]);
|
||||||
|
|
||||||
|
await sync_table.addNoteReorderingSync(beforeNote.parent_note_id, sourceId);
|
||||||
|
|
||||||
|
const now = utils.nowDate();
|
||||||
|
|
||||||
|
await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?",
|
||||||
|
[beforeNote.parent_note_id, beforeNote.note_position, now, noteTreeId]);
|
||||||
|
|
||||||
|
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send({ success: true });
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeId = req.params.noteTreeId;
|
const noteTreeId = req.params.noteTreeId;
|
||||||
const afterNoteTreeId = req.params.afterNoteTreeId;
|
const afterNoteTreeId = req.params.afterNoteTreeId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
const afterNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [afterNoteTreeId]);
|
const noteToMove = await getNoteTree(noteTreeId);
|
||||||
|
const afterNote = await getNoteTree(afterNoteTreeId);
|
||||||
|
|
||||||
if (afterNote) {
|
if (!await validateParentChild(res, afterNote.parent_note_id, noteToMove.note_id, noteTreeId)) {
|
||||||
await sql.doInTransaction(async () => {
|
return;
|
||||||
// we don't change date_modified so other changes are prioritized in case of conflict
|
|
||||||
// also we would have to sync all those modified note trees otherwise hash checks would fail
|
|
||||||
await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position > ? AND is_deleted = 0",
|
|
||||||
[afterNote.parent_note_id, afterNote.note_position]);
|
|
||||||
|
|
||||||
await sync_table.addNoteReorderingSync(afterNote.parent_note_id, sourceId);
|
|
||||||
|
|
||||||
await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?",
|
|
||||||
[afterNote.parent_note_id, afterNote.note_position + 1, utils.nowDate(), noteTreeId]);
|
|
||||||
|
|
||||||
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
|
||||||
});
|
|
||||||
|
|
||||||
res.send({});
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
res.status(500).send("After note " + afterNoteTreeId + " doesn't exist.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, async (req, res, next) => {
|
await sql.doInTransaction(async () => {
|
||||||
|
// we don't change date_modified so other changes are prioritized in case of conflict
|
||||||
|
// also we would have to sync all those modified note trees otherwise hash checks would fail
|
||||||
|
await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position > ? AND is_deleted = 0",
|
||||||
|
[afterNote.parent_note_id, afterNote.note_position]);
|
||||||
|
|
||||||
|
await sync_table.addNoteReorderingSync(afterNote.parent_note_id, sourceId);
|
||||||
|
|
||||||
|
await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?",
|
||||||
|
[afterNote.parent_note_id, afterNote.note_position + 1, utils.nowDate(), noteTreeId]);
|
||||||
|
|
||||||
|
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send({ success: true });
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const parentNoteId = req.params.parentNoteId;
|
const parentNoteId = req.params.parentNoteId;
|
||||||
const childNoteId = req.params.childNoteId;
|
const childNoteId = req.params.childNoteId;
|
||||||
const prefix = req.body.prefix;
|
const prefix = req.body.prefix;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
const existing = await sql.getFirst('SELECT * FROM notes_tree WHERE note_id = ? AND parent_note_id = ?', [childNoteId, parentNoteId]);
|
if (!await validateParentChild(res, parentNoteId, childNoteId)) {
|
||||||
|
return;
|
||||||
if (existing && !existing.is_deleted) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'This note already exists in target parent note.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await checkCycle(parentNoteId, childNoteId)) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'Cloning note here would create cycle.'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxNotePos = await sql.getFirstValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]);
|
const maxNotePos = await sql.getFirstValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]);
|
||||||
@@ -131,36 +131,18 @@ router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, async (req
|
|||||||
await sql.execute("UPDATE notes_tree SET is_expanded = 1 WHERE note_id = ?", [parentNoteId]);
|
await sql.execute("UPDATE notes_tree SET is_expanded = 1 WHERE note_id = ?", [parentNoteId]);
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send({
|
res.send({ success: true });
|
||||||
success: true
|
}));
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
const afterNoteTreeId = req.params.afterNoteTreeId;
|
const afterNoteTreeId = req.params.afterNoteTreeId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
const afterNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [afterNoteTreeId]);
|
const afterNote = await getNoteTree(afterNoteTreeId);
|
||||||
|
|
||||||
if (!afterNote) {
|
if (!await validateParentChild(res, afterNote.parent_note_id, noteId)) {
|
||||||
return res.status(500).send("After note " + afterNoteTreeId + " doesn't exist.");
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (!await checkCycle(afterNote.parent_note_id, noteId)) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'Cloning note here would create cycle.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const existing = await sql.getFirstValue('SELECT * FROM notes_tree WHERE note_id = ? AND parent_note_id = ?', [noteId, afterNote.parent_note_id]);
|
|
||||||
|
|
||||||
if (existing && !existing.is_deleted) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'This note already exists in target parent note.'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
@@ -186,32 +168,86 @@ router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, async (re
|
|||||||
await sync_table.addNoteTreeSync(noteTree.note_tree_id, sourceId);
|
await sync_table.addNoteTreeSync(noteTree.note_tree_id, sourceId);
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send({
|
res.send({ success: true });
|
||||||
success: true
|
}));
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function checkCycle(parentNoteId, childNoteId) {
|
async function loadSubTreeNoteIds(parentNoteId, subTreeNoteIds) {
|
||||||
if (parentNoteId === 'root') {
|
subTreeNoteIds.push(parentNoteId);
|
||||||
return true;
|
|
||||||
|
const children = await sql.getFirstColumn("SELECT note_id FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0", [parentNoteId]);
|
||||||
|
|
||||||
|
for (const childNoteId of children) {
|
||||||
|
await loadSubTreeNoteIds(childNoteId, subTreeNoteIds);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getNoteTree(noteTreeId) {
|
||||||
|
return sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateParentChild(res, parentNoteId, childNoteId, noteTreeId = null) {
|
||||||
|
const existing = await getExistingNoteTree(parentNoteId, childNoteId);
|
||||||
|
|
||||||
|
if (existing && (noteTreeId === null || existing.note_tree_id !== noteTreeId)) {
|
||||||
|
res.send({
|
||||||
|
success: false,
|
||||||
|
message: 'This note already exists in the target.'
|
||||||
|
});
|
||||||
|
|
||||||
if (parentNoteId === childNoteId) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentNoteIds = await sql.getFirstColumn("SELECT DISTINCT parent_note_id FROM notes_tree WHERE note_id = ?", [parentNoteId]);
|
if (!await checkTreeCycle(parentNoteId, childNoteId)) {
|
||||||
|
res.send({
|
||||||
|
success: false,
|
||||||
|
message: 'Moving note here would create cycle.'
|
||||||
|
});
|
||||||
|
|
||||||
for (const pid of parentNoteIds) {
|
return false;
|
||||||
if (!await checkCycle(pid, childNoteId)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
router.put('/:noteTreeId/expanded/:expanded', auth.checkApiAuth, async (req, res, next) => {
|
async function getExistingNoteTree(parentNoteId, childNoteId) {
|
||||||
|
return await sql.getFirst('SELECT * FROM notes_tree WHERE note_id = ? AND parent_note_id = ? AND is_deleted = 0', [childNoteId, parentNoteId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tree cycle can be created when cloning or when moving existing clone. This method should detect both cases.
|
||||||
|
*/
|
||||||
|
async function checkTreeCycle(parentNoteId, childNoteId) {
|
||||||
|
const subTreeNoteIds = [];
|
||||||
|
|
||||||
|
// we'll load the whole sub tree - because the cycle can start in one of the notes in the sub tree
|
||||||
|
await loadSubTreeNoteIds(childNoteId, subTreeNoteIds);
|
||||||
|
|
||||||
|
async function checkTreeCycleInner(parentNoteId) {
|
||||||
|
if (parentNoteId === 'root') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subTreeNoteIds.includes(parentNoteId)) {
|
||||||
|
// while towards the root of the tree we encountered noteId which is already present in the subtree
|
||||||
|
// joining parentNoteId with childNoteId would then clearly create a cycle
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentNoteIds = await sql.getFirstColumn("SELECT DISTINCT parent_note_id FROM notes_tree WHERE note_id = ? AND is_deleted = 0", [parentNoteId]);
|
||||||
|
|
||||||
|
for (const pid of parentNoteIds) {
|
||||||
|
if (!await checkTreeCycleInner(pid)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await checkTreeCycleInner(parentNoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
router.put('/:noteTreeId/expanded/:expanded', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeId = req.params.noteTreeId;
|
const noteTreeId = req.params.noteTreeId;
|
||||||
const expanded = req.params.expanded;
|
const expanded = req.params.expanded;
|
||||||
|
|
||||||
@@ -222,6 +258,6 @@ router.put('/:noteTreeId/expanded/:expanded', auth.checkApiAuth, async (req, res
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -5,11 +5,12 @@ const router = express.Router();
|
|||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
const changePassword = require('../../services/change_password');
|
const changePassword = require('../../services/change_password');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.post('/change', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/change', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const result = await changePassword.changePassword(req.body['current_password'], req.body['new_password'], req);
|
const result = await changePassword.changePassword(req.body['current_password'], req.body['new_password'], req);
|
||||||
|
|
||||||
res.send(result);
|
res.send(result);
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -4,8 +4,9 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const recentChanges = await sql.getAll(
|
const recentChanges = await sql.getAll(
|
||||||
`SELECT
|
`SELECT
|
||||||
notes.is_deleted AS current_is_deleted,
|
notes.is_deleted AS current_is_deleted,
|
||||||
@@ -19,6 +20,6 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
LIMIT 1000`);
|
LIMIT 1000`);
|
||||||
|
|
||||||
res.send(recentChanges);
|
res.send(recentChanges);
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -7,12 +7,13 @@ const auth = require('../../services/auth');
|
|||||||
const utils = require('../../services/utils');
|
const utils = require('../../services/utils');
|
||||||
const sync_table = require('../../services/sync_table');
|
const sync_table = require('../../services/sync_table');
|
||||||
const options = require('../../services/options');
|
const options = require('../../services/options');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('', auth.checkApiAuth, async (req, res, next) => {
|
router.get('', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
res.send(await getRecentNotes());
|
res.send(await getRecentNotes());
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/:noteTreeId/:notePath', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteTreeId/:notePath', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeId = req.params.noteTreeId;
|
const noteTreeId = req.params.noteTreeId;
|
||||||
const notePath = req.params.notePath;
|
const notePath = req.params.notePath;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
@@ -31,7 +32,7 @@ router.put('/:noteTreeId/:notePath', auth.checkApiAuth, async (req, res, next) =
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.send(await getRecentNotes());
|
res.send(await getRecentNotes());
|
||||||
});
|
}));
|
||||||
|
|
||||||
async function getRecentNotes() {
|
async function getRecentNotes() {
|
||||||
return await sql.getAll(`
|
return await sql.getAll(`
|
||||||
|
|||||||
@@ -5,24 +5,25 @@ const router = express.Router();
|
|||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
const options = require('../../services/options');
|
const options = require('../../services/options');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
// options allowed to be updated directly in settings dialog
|
// options allowed to be updated directly in settings dialog
|
||||||
const ALLOWED_OPTIONS = ['protected_session_timeout', 'history_snapshot_time_interval'];
|
const ALLOWED_OPTIONS = ['protected_session_timeout', 'history_snapshot_time_interval'];
|
||||||
|
|
||||||
router.get('/all', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/all', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const settings = await sql.getMap("SELECT opt_name, opt_value FROM options");
|
const settings = await sql.getMap("SELECT opt_name, opt_value FROM options");
|
||||||
|
|
||||||
res.send(settings);
|
res.send(settings);
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const settings = await sql.getMap("SELECT opt_name, opt_value FROM options WHERE opt_name IN ("
|
const settings = await sql.getMap("SELECT opt_name, opt_value FROM options WHERE opt_name IN ("
|
||||||
+ ALLOWED_OPTIONS.map(x => '?').join(",") + ")", ALLOWED_OPTIONS);
|
+ ALLOWED_OPTIONS.map(x => '?').join(",") + ")", ALLOWED_OPTIONS);
|
||||||
|
|
||||||
res.send(settings);
|
res.send(settings);
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.post('/', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const body = req.body;
|
const body = req.body;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
@@ -38,6 +39,6 @@ router.post('/', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
else {
|
else {
|
||||||
res.send("not allowed option to set");
|
res.send("not allowed option to set");
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -8,8 +8,9 @@ const sql = require('../../services/sql');
|
|||||||
const utils = require('../../services/utils');
|
const utils = require('../../services/utils');
|
||||||
const my_scrypt = require('../../services/my_scrypt');
|
const my_scrypt = require('../../services/my_scrypt');
|
||||||
const password_encryption = require('../../services/password_encryption');
|
const password_encryption = require('../../services/password_encryption');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.post('', auth.checkAppNotInitialized, async (req, res, next) => {
|
router.post('', auth.checkAppNotInitialized, wrap(async (req, res, next) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
@@ -27,6 +28,6 @@ router.post('', auth.checkAppNotInitialized, async (req, res, next) => {
|
|||||||
sql.setDbReadyAsResolved();
|
sql.setDbReadyAsResolved();
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -4,8 +4,9 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.post('/execute', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/execute', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const query = req.body.query;
|
const query = req.body.query;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -20,6 +21,6 @@ router.post('/execute', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
error: e.message
|
error: e.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -10,19 +10,20 @@ const sql = require('../../services/sql');
|
|||||||
const options = require('../../services/options');
|
const options = require('../../services/options');
|
||||||
const content_hash = require('../../services/content_hash');
|
const content_hash = require('../../services/content_hash');
|
||||||
const log = require('../../services/log');
|
const log = require('../../services/log');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('/check', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/check', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
res.send({
|
res.send({
|
||||||
'hashes': await content_hash.getHashes(),
|
'hashes': await content_hash.getHashes(),
|
||||||
'max_sync_id': await sql.getFirstValue('SELECT MAX(id) FROM sync')
|
'max_sync_id': await sql.getFirstValue('SELECT MAX(id) FROM sync')
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.post('/now', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/now', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
res.send(await sync.sync());
|
res.send(await sync.sync());
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.post('/fill-sync-rows', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/fill-sync-rows', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
await sync_table.fillAllSyncRows();
|
await sync_table.fillAllSyncRows();
|
||||||
});
|
});
|
||||||
@@ -30,9 +31,9 @@ router.post('/fill-sync-rows', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
log.info("Sync rows have been filled.");
|
log.info("Sync rows have been filled.");
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.post('/force-full-sync', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/force-full-sync', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
await options.setOption('last_synced_pull', 0);
|
await options.setOption('last_synced_pull', 0);
|
||||||
await options.setOption('last_synced_push', 0);
|
await options.setOption('last_synced_push', 0);
|
||||||
@@ -44,35 +45,59 @@ router.post('/force-full-sync', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
sync.sync();
|
sync.sync();
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/changed', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/force-note-sync/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
|
const noteId = req.params.noteId;
|
||||||
|
|
||||||
|
await sql.doInTransaction(async () => {
|
||||||
|
await sync_table.addNoteSync(noteId);
|
||||||
|
|
||||||
|
for (const noteTreeId of await sql.getFirstColumn("SELECT note_tree_id FROM notes_tree WHERE is_deleted = 0 AND note_id = ?", [noteId])) {
|
||||||
|
await sync_table.addNoteTreeSync(noteTreeId);
|
||||||
|
await sync_table.addRecentNoteSync(noteTreeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const noteHistoryId of await sql.getFirstColumn("SELECT note_history_id FROM notes_history WHERE note_id = ?", [noteId])) {
|
||||||
|
await sync_table.addNoteHistorySync(noteHistoryId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("Forcing note sync for " + noteId);
|
||||||
|
|
||||||
|
// not awaiting for the job to finish (will probably take a long time)
|
||||||
|
sync.sync();
|
||||||
|
|
||||||
|
res.send({});
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.get('/changed', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const lastSyncId = parseInt(req.query.lastSyncId);
|
const lastSyncId = parseInt(req.query.lastSyncId);
|
||||||
|
|
||||||
res.send(await sql.getAll("SELECT * FROM sync WHERE id > ?", [lastSyncId]));
|
res.send(await sql.getAll("SELECT * FROM sync WHERE id > ?", [lastSyncId]));
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/notes/:noteId', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/notes/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
entity: await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [noteId])
|
entity: await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [noteId])
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/notes_tree/:noteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/notes_tree/:noteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeId = req.params.noteTreeId;
|
const noteTreeId = req.params.noteTreeId;
|
||||||
|
|
||||||
res.send(await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]));
|
res.send(await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]));
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/notes_history/:noteHistoryId', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/notes_history/:noteHistoryId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteHistoryId = req.params.noteHistoryId;
|
const noteHistoryId = req.params.noteHistoryId;
|
||||||
|
|
||||||
res.send(await sql.getFirst("SELECT * FROM notes_history WHERE note_history_id = ?", [noteHistoryId]));
|
res.send(await sql.getFirst("SELECT * FROM notes_history WHERE note_history_id = ?", [noteHistoryId]));
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/options/:optName', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/options/:optName', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const optName = req.params.optName;
|
const optName = req.params.optName;
|
||||||
|
|
||||||
if (!options.SYNCED_OPTIONS.includes(optName)) {
|
if (!options.SYNCED_OPTIONS.includes(optName)) {
|
||||||
@@ -81,57 +106,86 @@ router.get('/options/:optName', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
else {
|
else {
|
||||||
res.send(await sql.getFirst("SELECT * FROM options WHERE opt_name = ?", [optName]));
|
res.send(await sql.getFirst("SELECT * FROM options WHERE opt_name = ?", [optName]));
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/notes_reordering/:noteTreeParentId', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/notes_reordering/:noteTreeParentId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeParentId = req.params.noteTreeParentId;
|
const noteTreeParentId = req.params.noteTreeParentId;
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
parent_note_id: noteTreeParentId,
|
parent_note_id: noteTreeParentId,
|
||||||
ordering: await sql.getMap("SELECT note_tree_id, note_position FROM notes_tree WHERE parent_note_id = ?", [noteTreeParentId])
|
ordering: await sql.getMap("SELECT note_tree_id, note_position FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0", [noteTreeParentId])
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/recent_notes/:noteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/recent_notes/:noteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeId = req.params.noteTreeId;
|
const noteTreeId = req.params.noteTreeId;
|
||||||
|
|
||||||
res.send(await sql.getFirst("SELECT * FROM recent_notes WHERE note_tree_id = ?", [noteTreeId]));
|
res.send(await sql.getFirst("SELECT * FROM recent_notes WHERE note_tree_id = ?", [noteTreeId]));
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/notes', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/images/:imageId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
|
const imageId = req.params.imageId;
|
||||||
|
const entity = await sql.getFirst("SELECT * FROM images WHERE image_id = ?", [imageId]);
|
||||||
|
|
||||||
|
if (entity && entity.data !== null) {
|
||||||
|
entity.data = entity.data.toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(entity);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.get('/notes_image/:noteImageId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
|
const noteImageId = req.params.noteImageId;
|
||||||
|
|
||||||
|
res.send(await sql.getFirst("SELECT * FROM notes_image WHERE note_image_id = ?", [noteImageId]));
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.put('/notes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await syncUpdate.updateNote(req.body.entity, req.body.sourceId);
|
await syncUpdate.updateNote(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/notes_tree', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/notes_tree', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await syncUpdate.updateNoteTree(req.body.entity, req.body.sourceId);
|
await syncUpdate.updateNoteTree(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/notes_history', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/notes_history', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await syncUpdate.updateNoteHistory(req.body.entity, req.body.sourceId);
|
await syncUpdate.updateNoteHistory(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/notes_reordering', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/notes_reordering', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await syncUpdate.updateNoteReordering(req.body.entity, req.body.sourceId);
|
await syncUpdate.updateNoteReordering(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/options', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/options', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await syncUpdate.updateOptions(req.body.entity, req.body.sourceId);
|
await syncUpdate.updateOptions(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/recent_notes', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/recent_notes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await syncUpdate.updateRecentNotes(req.body.entity, req.body.sourceId);
|
await syncUpdate.updateRecentNotes(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
|
router.put('/images', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
|
await syncUpdate.updateImage(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
|
res.send({});
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.put('/notes_image', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
|
await syncUpdate.updateNoteImage(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
|
res.send({});
|
||||||
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -10,16 +10,23 @@ const protected_session = require('../../services/protected_session');
|
|||||||
const data_encryption = require('../../services/data_encryption');
|
const data_encryption = require('../../services/data_encryption');
|
||||||
const notes = require('../../services/notes');
|
const notes = require('../../services/notes');
|
||||||
const sync_table = require('../../services/sync_table');
|
const sync_table = require('../../services/sync_table');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const notes = await sql.getAll("SELECT "
|
const notes = await sql.getAll(`
|
||||||
+ "notes_tree.*, "
|
SELECT
|
||||||
+ "notes.note_title, "
|
notes_tree.*,
|
||||||
+ "notes.is_protected "
|
notes.note_title,
|
||||||
+ "FROM notes_tree "
|
notes.is_protected
|
||||||
+ "JOIN notes ON notes.note_id = notes_tree.note_id "
|
FROM
|
||||||
+ "WHERE notes.is_deleted = 0 AND notes_tree.is_deleted = 0 "
|
notes_tree
|
||||||
+ "ORDER BY note_position");
|
JOIN
|
||||||
|
notes ON notes.note_id = notes_tree.note_id
|
||||||
|
WHERE
|
||||||
|
notes.is_deleted = 0
|
||||||
|
AND notes_tree.is_deleted = 0
|
||||||
|
ORDER BY
|
||||||
|
note_position`);
|
||||||
|
|
||||||
const dataKey = protected_session.getDataKey(req);
|
const dataKey = protected_session.getDataKey(req);
|
||||||
|
|
||||||
@@ -33,9 +40,9 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
notes: notes,
|
notes: notes,
|
||||||
start_note_path: await options.getOption('start_note_path')
|
start_note_path: await options.getOption('start_note_path')
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/:noteId/protect-sub-tree/:isProtected', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteId/protect-sub-tree/:isProtected', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
const isProtected = !!parseInt(req.params.isProtected);
|
const isProtected = !!parseInt(req.params.isProtected);
|
||||||
const dataKey = protected_session.getDataKey(req);
|
const dataKey = protected_session.getDataKey(req);
|
||||||
@@ -46,9 +53,9 @@ router.put('/:noteId/protect-sub-tree/:isProtected', auth.checkApiAuth, async (r
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/:noteTreeId/set-prefix', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteTreeId/set-prefix', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeId = req.params.noteTreeId;
|
const noteTreeId = req.params.noteTreeId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
const prefix = utils.isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix;
|
const prefix = utils.isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix;
|
||||||
@@ -60,6 +67,6 @@ router.put('/:noteTreeId/set-prefix', auth.checkApiAuth, async (req, res, next)
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ const router = express.Router();
|
|||||||
const auth = require('../services/auth');
|
const auth = require('../services/auth');
|
||||||
const source_id = require('../services/source_id');
|
const source_id = require('../services/source_id');
|
||||||
const sql = require('../services/sql');
|
const sql = require('../services/sql');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('', auth.checkAuth, async (req, res, next) => {
|
router.get('', auth.checkAuth, wrap(async (req, res, next) => {
|
||||||
res.render('index', {
|
res.render('index', {
|
||||||
sourceId: await source_id.generateSourceId(),
|
sourceId: await source_id.generateSourceId(),
|
||||||
maxSyncIdAtLoad: await sql.getFirstValue("SELECT MAX(id) FROM sync")
|
maxSyncIdAtLoad: await sql.getFirstValue("SELECT MAX(id) FROM sync")
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ const router = express.Router();
|
|||||||
const utils = require('../services/utils');
|
const utils = require('../services/utils');
|
||||||
const options = require('../services/options');
|
const options = require('../services/options');
|
||||||
const my_scrypt = require('../services/my_scrypt');
|
const my_scrypt = require('../services/my_scrypt');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('', (req, res, next) => {
|
router.get('', wrap(async (req, res, next) => {
|
||||||
res.render('login', { 'failedAuth': false });
|
res.render('login', { 'failedAuth': false });
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.post('', async (req, res, next) => {
|
router.post('', wrap(async (req, res, next) => {
|
||||||
const userName = await options.getOption('username');
|
const userName = await options.getOption('username');
|
||||||
|
|
||||||
const guessedPassword = req.body.password;
|
const guessedPassword = req.body.password;
|
||||||
@@ -32,7 +33,7 @@ router.post('', async (req, res, next) => {
|
|||||||
else {
|
else {
|
||||||
res.render('login', {'failedAuth': true});
|
res.render('login', {'failedAuth': true});
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
|
|
||||||
async function verifyPassword(guessed_password) {
|
async function verifyPassword(guessed_password) {
|
||||||
|
|||||||
@@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.post('', async (req, res, next) => {
|
router.post('', wrap(async (req, res, next) => {
|
||||||
req.session.regenerate(() => {
|
req.session.regenerate(() => {
|
||||||
req.session.loggedIn = false;
|
req.session.loggedIn = false;
|
||||||
|
|
||||||
res.redirect('/');
|
res.redirect('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const auth = require('../services/auth');
|
const auth = require('../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('', auth.checkAuthForMigrationPage, (req, res, next) => {
|
router.get('', auth.checkAuthForMigrationPage, wrap(async (req, res, next) => {
|
||||||
res.render('migration', {});
|
res.render('migration', {});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const setupApiRoute = require('./api/setup');
|
|||||||
const sqlRoute = require('./api/sql');
|
const sqlRoute = require('./api/sql');
|
||||||
const anonymizationRoute = require('./api/anonymization');
|
const anonymizationRoute = require('./api/anonymization');
|
||||||
const cleanupRoute = require('./api/cleanup');
|
const cleanupRoute = require('./api/cleanup');
|
||||||
|
const imageRoute = require('./api/image');
|
||||||
|
|
||||||
function register(app) {
|
function register(app) {
|
||||||
app.use('/', indexRoute);
|
app.use('/', indexRoute);
|
||||||
@@ -51,6 +52,7 @@ function register(app) {
|
|||||||
app.use('/api/sql', sqlRoute);
|
app.use('/api/sql', sqlRoute);
|
||||||
app.use('/api/anonymization', anonymizationRoute);
|
app.use('/api/anonymization', anonymizationRoute);
|
||||||
app.use('/api/cleanup', cleanupRoute);
|
app.use('/api/cleanup', cleanupRoute);
|
||||||
|
app.use('/api/images', imageRoute);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const auth = require('../services/auth');
|
const auth = require('../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('', auth.checkAppNotInitialized, (req, res, next) => {
|
router.get('', auth.checkAppNotInitialized, wrap(async (req, res, next) => {
|
||||||
res.render('setup', {});
|
res.render('setup', {});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
const build = require('./build');
|
const build = require('./build');
|
||||||
const packageJson = require('../package');
|
const packageJson = require('../package');
|
||||||
|
|
||||||
const APP_DB_VERSION = 60;
|
const APP_DB_VERSION = 65;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
app_version: packageJson.version,
|
app_version: packageJson.version,
|
||||||
|
|||||||
@@ -3,12 +3,9 @@
|
|||||||
const migration = require('./migration');
|
const migration = require('./migration');
|
||||||
const sql = require('./sql');
|
const sql = require('./sql');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const options = require('./options');
|
|
||||||
|
|
||||||
async function checkAuth(req, res, next) {
|
async function checkAuth(req, res, next) {
|
||||||
const username = await options.getOption('username');
|
if (!await sql.isUserInitialized()) {
|
||||||
|
|
||||||
if (!username) {
|
|
||||||
res.redirect("setup");
|
res.redirect("setup");
|
||||||
}
|
}
|
||||||
else if (!req.session.loggedIn && !utils.isElectron()) {
|
else if (!req.session.loggedIn && !utils.isElectron()) {
|
||||||
@@ -31,6 +28,20 @@ async function checkAuthForMigrationPage(req, res, next) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for electron things which need network stuff
|
||||||
|
// currently we're doing that for file upload because handling form data seems to be difficult
|
||||||
|
async function checkApiAuthOrElectron(req, res, next) {
|
||||||
|
if (!req.session.loggedIn && !utils.isElectron()) {
|
||||||
|
res.status(401).send("Not authorized");
|
||||||
|
}
|
||||||
|
else if (await sql.isDbUpToDate()) {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
res.status(409).send("Mismatched app versions"); // need better response than that
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function checkApiAuth(req, res, next) {
|
async function checkApiAuth(req, res, next) {
|
||||||
if (!req.session.loggedIn) {
|
if (!req.session.loggedIn) {
|
||||||
res.status(401).send("Not authorized");
|
res.status(401).send("Not authorized");
|
||||||
@@ -53,9 +64,7 @@ async function checkApiAuthForMigrationPage(req, res, next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkAppNotInitialized(req, res, next) {
|
async function checkAppNotInitialized(req, res, next) {
|
||||||
const username = await options.getOption('username');
|
if (await sql.isUserInitialized()) {
|
||||||
|
|
||||||
if (username) {
|
|
||||||
res.status(400).send("App already initialized.");
|
res.status(400).send("App already initialized.");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -68,5 +77,6 @@ module.exports = {
|
|||||||
checkAuthForMigrationPage,
|
checkAuthForMigrationPage,
|
||||||
checkApiAuth,
|
checkApiAuth,
|
||||||
checkApiAuthForMigrationPage,
|
checkApiAuthForMigrationPage,
|
||||||
checkAppNotInitialized
|
checkAppNotInitialized,
|
||||||
|
checkApiAuthOrElectron
|
||||||
};
|
};
|
||||||
@@ -6,6 +6,7 @@ const fs = require('fs-extra');
|
|||||||
const dataDir = require('./data_dir');
|
const dataDir = require('./data_dir');
|
||||||
const log = require('./log');
|
const log = require('./log');
|
||||||
const sql = require('./sql');
|
const sql = require('./sql');
|
||||||
|
const sync_mutex = require('./sync_mutex');
|
||||||
|
|
||||||
async function regularBackup() {
|
async function regularBackup() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -21,17 +22,25 @@ async function regularBackup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function backupNow() {
|
async function backupNow() {
|
||||||
const now = utils.nowDate();
|
// we don't want to backup DB in the middle of sync with potentially inconsistent DB state
|
||||||
|
const releaseMutex = await sync_mutex.acquire();
|
||||||
|
|
||||||
const backupFile = dataDir.BACKUP_DIR + "/" + "backup-" + utils.getDateTimeForFile() + ".db";
|
try {
|
||||||
|
const now = utils.nowDate();
|
||||||
|
|
||||||
fs.copySync(dataDir.DOCUMENT_PATH, backupFile);
|
const backupFile = dataDir.BACKUP_DIR + "/" + "backup-" + utils.getDateTimeForFile() + ".db";
|
||||||
|
|
||||||
log.info("Created backup at " + backupFile);
|
fs.copySync(dataDir.DOCUMENT_PATH, backupFile);
|
||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
log.info("Created backup at " + backupFile);
|
||||||
await options.setOption('last_backup_date', now);
|
|
||||||
});
|
await sql.doInTransaction(async () => {
|
||||||
|
await options.setOption('last_backup_date', now);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
releaseMutex();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cleanupOldBackups() {
|
async function cleanupOldBackups() {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
module.exports = { build_date:"2017-12-23T14:02:07-05:00", build_revision: "51215cba1bd2da8a539e86dbd31004cf9adb3f93" };
|
module.exports = { build_date:"2018-01-07T10:04:46-05:00", build_revision: "488e657cc43fab879662a6da23b5a69dd7591b06" };
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ const ini = require('ini');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const dataDir = require('./data_dir');
|
const dataDir = require('./data_dir');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const resource_dir = require('./resource_dir');
|
||||||
|
|
||||||
const configSampleFilePath = path.resolve(__dirname, "..", "config-sample.ini");
|
const configSampleFilePath = path.resolve(resource_dir.RESOURCE_DIR, "config-sample.ini");
|
||||||
|
|
||||||
const configFilePath = dataDir.TRILIUM_DATA_DIR + '/config.ini';
|
const configFilePath = dataDir.TRILIUM_DATA_DIR + '/config.ini';
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,12 @@
|
|||||||
const sql = require('./sql');
|
const sql = require('./sql');
|
||||||
const log = require('./log');
|
const log = require('./log');
|
||||||
const messaging = require('./messaging');
|
const messaging = require('./messaging');
|
||||||
|
const sync_mutex = require('./sync_mutex');
|
||||||
|
const utils = require('./utils');
|
||||||
|
|
||||||
async function runCheck(query, errorText, errorList) {
|
async function runCheck(query, errorText, errorList) {
|
||||||
|
utils.assertArguments(query, errorText, errorList);
|
||||||
|
|
||||||
const result = await sql.getFirstColumn(query);
|
const result = await sql.getFirstColumn(query);
|
||||||
|
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
@@ -17,6 +21,46 @@ async function runCheck(query, errorText, errorList) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkTreeCycles(errorList) {
|
||||||
|
const childToParents = {};
|
||||||
|
const rows = await sql.getAll("SELECT note_id, parent_note_id FROM notes_tree WHERE is_deleted = 0");
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
const childNoteId = row.note_id;
|
||||||
|
const parentNoteId = row.parent_note_id;
|
||||||
|
|
||||||
|
if (!childToParents[childNoteId]) {
|
||||||
|
childToParents[childNoteId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
childToParents[childNoteId].push(parentNoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkTreeCycle(noteId, path, errorList) {
|
||||||
|
if (noteId === 'root') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const parentNoteId of childToParents[noteId]) {
|
||||||
|
if (path.includes(parentNoteId)) {
|
||||||
|
errorList.push(`Tree cycle detected at parent-child relationship: ${parentNoteId} - ${noteId}, whole path: ${path}`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const newPath = path.slice();
|
||||||
|
newPath.push(noteId);
|
||||||
|
|
||||||
|
checkTreeCycle(parentNoteId, newPath, errorList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const noteIds = Object.keys(childToParents);
|
||||||
|
|
||||||
|
for (const noteId of noteIds) {
|
||||||
|
checkTreeCycle(noteId, [], errorList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function runSyncRowChecks(table, key, errorList) {
|
async function runSyncRowChecks(table, key, errorList) {
|
||||||
await runCheck(`
|
await runCheck(`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -40,7 +84,7 @@ async function runSyncRowChecks(table, key, errorList) {
|
|||||||
`Missing ${table} records for existing sync rows`, errorList);
|
`Missing ${table} records for existing sync rows`, errorList);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runChecks() {
|
async function runAllChecks() {
|
||||||
const errorList = [];
|
const errorList = [];
|
||||||
|
|
||||||
await runCheck(`
|
await runCheck(`
|
||||||
@@ -97,7 +141,7 @@ async function runChecks() {
|
|||||||
WHERE
|
WHERE
|
||||||
(SELECT COUNT(*) FROM notes_tree WHERE notes.note_id = notes_tree.note_id AND notes_tree.is_deleted = 0) = 0
|
(SELECT COUNT(*) FROM notes_tree WHERE notes.note_id = notes_tree.note_id AND notes_tree.is_deleted = 0) = 0
|
||||||
AND notes.is_deleted = 0
|
AND notes.is_deleted = 0
|
||||||
`, );
|
`, 'No undeleted note trees for note IDs', errorList);
|
||||||
|
|
||||||
await runCheck(`
|
await runCheck(`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -119,16 +163,69 @@ async function runChecks() {
|
|||||||
notes.note_id IS NULL`,
|
notes.note_id IS NULL`,
|
||||||
"Missing notes records for following note history ID > note ID", errorList);
|
"Missing notes records for following note history ID > note ID", errorList);
|
||||||
|
|
||||||
|
await runCheck(`
|
||||||
|
SELECT
|
||||||
|
notes_tree.parent_note_id || ' > ' || notes_tree.note_id
|
||||||
|
FROM
|
||||||
|
notes_tree
|
||||||
|
WHERE
|
||||||
|
notes_tree.is_deleted = 0
|
||||||
|
GROUP BY
|
||||||
|
notes_tree.parent_note_id,
|
||||||
|
notes_tree.note_id
|
||||||
|
HAVING
|
||||||
|
COUNT(*) > 1`,
|
||||||
|
"Duplicate undeleted parent note <-> note relationship - parent note ID > note ID", errorList);
|
||||||
|
|
||||||
|
await runCheck(`
|
||||||
|
SELECT
|
||||||
|
images.image_id
|
||||||
|
FROM
|
||||||
|
images
|
||||||
|
LEFT JOIN notes_image ON notes_image.image_id = images.image_id
|
||||||
|
WHERE
|
||||||
|
notes_image.note_image_id IS NULL`,
|
||||||
|
"Image with no note relation", errorList);
|
||||||
|
|
||||||
await runSyncRowChecks("notes", "note_id", errorList);
|
await runSyncRowChecks("notes", "note_id", errorList);
|
||||||
await runSyncRowChecks("notes_history", "note_history_id", errorList);
|
await runSyncRowChecks("notes_history", "note_history_id", errorList);
|
||||||
await runSyncRowChecks("notes_tree", "note_tree_id", errorList);
|
await runSyncRowChecks("notes_tree", "note_tree_id", errorList);
|
||||||
await runSyncRowChecks("recent_notes", "note_tree_id", errorList);
|
await runSyncRowChecks("recent_notes", "note_tree_id", errorList);
|
||||||
|
await runSyncRowChecks("images", "image_id", errorList);
|
||||||
|
await runSyncRowChecks("notes_image", "note_image_id", errorList);
|
||||||
|
|
||||||
|
if (errorList.length === 0) {
|
||||||
|
// we run this only if basic checks passed since this assumes basic data consistency
|
||||||
|
|
||||||
|
await checkTreeCycles(errorList);
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorList;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runChecks() {
|
||||||
|
let errorList;
|
||||||
|
let elapsedTimeMs;
|
||||||
|
const releaseMutex = await sync_mutex.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const startTime = new Date();
|
||||||
|
|
||||||
|
errorList = await runAllChecks();
|
||||||
|
|
||||||
|
elapsedTimeMs = new Date().getTime() - startTime.getTime();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
releaseMutex();
|
||||||
|
}
|
||||||
|
|
||||||
if (errorList.length > 0) {
|
if (errorList.length > 0) {
|
||||||
|
log.info(`Consistency checks failed (took ${elapsedTimeMs}ms) with these errors: ` + JSON.stringify(errorList));
|
||||||
|
|
||||||
messaging.sendMessageToAllClients({type: 'consistency-checks-failed'});
|
messaging.sendMessageToAllClients({type: 'consistency-checks-failed'});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.info("All consistency checks passed.");
|
log.info(`All consistency checks passed (took ${elapsedTimeMs}ms)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const sql = require('./sql');
|
const sql = require('./sql');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const options = require('./options');
|
const options = require('./options');
|
||||||
|
const log = require('./log');
|
||||||
|
|
||||||
function getHash(rows) {
|
function getHash(rows) {
|
||||||
let hash = '';
|
let hash = '';
|
||||||
@@ -13,55 +14,82 @@ function getHash(rows) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getHashes() {
|
async function getHashes() {
|
||||||
|
const startTime = new Date();
|
||||||
|
|
||||||
const optionsQuestionMarks = Array(options.SYNCED_OPTIONS.length).fill('?').join(',');
|
const optionsQuestionMarks = Array(options.SYNCED_OPTIONS.length).fill('?').join(',');
|
||||||
|
|
||||||
return {
|
const hashes = {
|
||||||
notes: getHash(await sql.getAll(`SELECT
|
notes: getHash(await sql.getAll(`
|
||||||
note_id,
|
SELECT
|
||||||
note_title,
|
note_id,
|
||||||
note_text,
|
note_title,
|
||||||
date_modified,
|
note_text,
|
||||||
is_protected,
|
date_modified,
|
||||||
is_deleted
|
is_protected,
|
||||||
FROM notes
|
is_deleted
|
||||||
ORDER BY note_id`)),
|
FROM notes
|
||||||
|
ORDER BY note_id`)),
|
||||||
|
|
||||||
notes_tree: getHash(await sql.getAll(`SELECT
|
notes_tree: getHash(await sql.getAll(`
|
||||||
note_tree_id,
|
SELECT
|
||||||
note_id,
|
note_tree_id,
|
||||||
parent_note_id,
|
note_id,
|
||||||
note_position,
|
parent_note_id,
|
||||||
date_modified,
|
note_position,
|
||||||
is_deleted,
|
date_modified,
|
||||||
prefix
|
is_deleted,
|
||||||
FROM notes_tree
|
prefix
|
||||||
ORDER BY note_tree_id`)),
|
FROM notes_tree
|
||||||
|
ORDER BY note_tree_id`)),
|
||||||
|
|
||||||
notes_history: getHash(await sql.getAll(`SELECT
|
notes_history: getHash(await sql.getAll(`
|
||||||
note_history_id,
|
SELECT
|
||||||
note_id,
|
note_history_id,
|
||||||
note_title,
|
note_id,
|
||||||
note_text,
|
note_title,
|
||||||
date_modified_from,
|
note_text,
|
||||||
date_modified_to
|
date_modified_from,
|
||||||
FROM notes_history
|
date_modified_to
|
||||||
ORDER BY note_history_id`)),
|
FROM notes_history
|
||||||
|
ORDER BY note_history_id`)),
|
||||||
|
|
||||||
recent_notes: getHash(await sql.getAll(`SELECT
|
recent_notes: getHash(await sql.getAll(`
|
||||||
note_tree_id,
|
SELECT
|
||||||
note_path,
|
note_tree_id,
|
||||||
date_accessed,
|
note_path,
|
||||||
is_deleted
|
date_accessed,
|
||||||
FROM recent_notes
|
is_deleted
|
||||||
ORDER BY note_path`)),
|
FROM recent_notes
|
||||||
|
ORDER BY note_path`)),
|
||||||
|
|
||||||
options: getHash(await sql.getAll(`SELECT
|
options: getHash(await sql.getAll(`
|
||||||
opt_name,
|
SELECT
|
||||||
opt_value
|
opt_name,
|
||||||
FROM options
|
opt_value
|
||||||
WHERE opt_name IN (${optionsQuestionMarks})
|
FROM options
|
||||||
ORDER BY opt_name`, options.SYNCED_OPTIONS))
|
WHERE opt_name IN (${optionsQuestionMarks})
|
||||||
|
ORDER BY opt_name`, options.SYNCED_OPTIONS)),
|
||||||
|
|
||||||
|
// we don't include image data on purpose because they are quite large, checksum is good enough
|
||||||
|
// to represent the data anyway
|
||||||
|
images: getHash(await sql.getAll(`
|
||||||
|
SELECT
|
||||||
|
image_id,
|
||||||
|
format,
|
||||||
|
checksum,
|
||||||
|
name,
|
||||||
|
is_deleted,
|
||||||
|
date_modified,
|
||||||
|
date_created
|
||||||
|
FROM images
|
||||||
|
ORDER BY image_id`))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const elapseTimeMs = new Date().getTime() - startTime.getTime();
|
||||||
|
|
||||||
|
log.info(`Content hash computation took ${elapseTimeMs}ms`);
|
||||||
|
|
||||||
|
return hashes;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
const log = require('./log');
|
||||||
|
|
||||||
function arraysIdentical(a, b) {
|
function arraysIdentical(a, b) {
|
||||||
let i = a.length;
|
let i = a.length;
|
||||||
@@ -72,7 +73,15 @@ function decrypt(key, iv, cipherText) {
|
|||||||
function decryptString(dataKey, iv, cipherText) {
|
function decryptString(dataKey, iv, cipherText) {
|
||||||
const buffer = decrypt(dataKey, iv, cipherText);
|
const buffer = decrypt(dataKey, iv, cipherText);
|
||||||
|
|
||||||
return buffer.toString('utf-8');
|
const str = buffer.toString('utf-8');
|
||||||
|
|
||||||
|
if (str === 'false') {
|
||||||
|
log.error("Could not decrypt string. Buffer: " + buffer);
|
||||||
|
|
||||||
|
throw new Error("Could not decrypt string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
function noteTitleIv(iv) {
|
function noteTitleIv(iv) {
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ async function sendMessage(client, message) {
|
|||||||
async function sendMessageToAllClients(message) {
|
async function sendMessageToAllClients(message) {
|
||||||
const jsonStr = JSON.stringify(message);
|
const jsonStr = JSON.stringify(message);
|
||||||
|
|
||||||
|
log.info("Sending message to all clients: " + jsonStr);
|
||||||
|
|
||||||
webSocketServer.clients.forEach(function each(client) {
|
webSocketServer.clients.forEach(function each(client) {
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
client.send(jsonStr);
|
client.send(jsonStr);
|
||||||
|
|||||||
@@ -3,14 +3,7 @@ const sql = require('./sql');
|
|||||||
const options = require('./options');
|
const options = require('./options');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const log = require('./log');
|
const log = require('./log');
|
||||||
const path = require('path');
|
const resource_dir = require('./resource_dir');
|
||||||
|
|
||||||
const MIGRATIONS_DIR = path.resolve(__dirname, "..", "migrations");
|
|
||||||
|
|
||||||
if (!fs.existsSync(MIGRATIONS_DIR)) {
|
|
||||||
log.error("Could not find migration directory: " + MIGRATIONS_DIR);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function migrate() {
|
async function migrate() {
|
||||||
const migrations = [];
|
const migrations = [];
|
||||||
@@ -20,7 +13,7 @@ async function migrate() {
|
|||||||
|
|
||||||
const currentDbVersion = parseInt(await options.getOption('db_version'));
|
const currentDbVersion = parseInt(await options.getOption('db_version'));
|
||||||
|
|
||||||
fs.readdirSync(MIGRATIONS_DIR).forEach(file => {
|
fs.readdirSync(resource_dir.MIGRATIONS_DIR).forEach(file => {
|
||||||
const match = file.match(/([0-9]{4})__([a-zA-Z0-9_ ]+)\.(sql|js)/);
|
const match = file.match(/([0-9]{4})__([a-zA-Z0-9_ ]+)\.(sql|js)/);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -53,7 +46,7 @@ async function migrate() {
|
|||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
if (mig.type === 'sql') {
|
if (mig.type === 'sql') {
|
||||||
const migrationSql = fs.readFileSync(MIGRATIONS_DIR + "/" + mig.file).toString('utf8');
|
const migrationSql = fs.readFileSync(resource_dir.MIGRATIONS_DIR + "/" + mig.file).toString('utf8');
|
||||||
|
|
||||||
console.log("Migration with SQL script: " + migrationSql);
|
console.log("Migration with SQL script: " + migrationSql);
|
||||||
|
|
||||||
@@ -62,7 +55,7 @@ async function migrate() {
|
|||||||
else if (mig.type === 'js') {
|
else if (mig.type === 'js') {
|
||||||
console.log("Migration with JS module");
|
console.log("Migration with JS module");
|
||||||
|
|
||||||
const migrationModule = require("../" + MIGRATIONS_DIR + "/" + mig.file);
|
const migrationModule = require("../" + resource_dir.MIGRATIONS_DIR + "/" + mig.file);
|
||||||
await migrationModule(db);
|
await migrationModule(db);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ async function protectNoteRecursively(noteId, dataKey, protect, sourceId) {
|
|||||||
|
|
||||||
await protectNote(note, dataKey, protect, sourceId);
|
await protectNote(note, dataKey, protect, sourceId);
|
||||||
|
|
||||||
const children = await sql.getFirstColumn("SELECT note_id FROM notes_tree WHERE parent_note_id = ?", [noteId]);
|
const children = await sql.getFirstColumn("SELECT note_id FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0", [noteId]);
|
||||||
|
|
||||||
for (const childNoteId of children) {
|
for (const childNoteId of children) {
|
||||||
await protectNoteRecursively(childNoteId, dataKey, protect, sourceId);
|
await protectNoteRecursively(childNoteId, dataKey, protect, sourceId);
|
||||||
@@ -104,8 +104,6 @@ async function protectNote(note, dataKey, protect, sourceId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
console.log("Updating...");
|
|
||||||
|
|
||||||
await sql.execute("UPDATE notes SET note_title = ?, note_text = ?, is_protected = ? WHERE note_id = ?",
|
await sql.execute("UPDATE notes SET note_title = ?, note_text = ?, is_protected = ? WHERE note_id = ?",
|
||||||
[note.note_title, note.note_text, note.is_protected, note.note_id]);
|
[note.note_title, note.note_text, note.is_protected, note.note_id]);
|
||||||
|
|
||||||
@@ -137,6 +135,76 @@ async function protectNoteHistory(noteId, dataKey, protect, sourceId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) {
|
||||||
|
const oldNote = await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [noteId]);
|
||||||
|
|
||||||
|
if (oldNote.is_protected) {
|
||||||
|
decryptNote(oldNote, dataKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newNoteHistoryId = utils.newNoteHistoryId();
|
||||||
|
|
||||||
|
await sql.insert('notes_history', {
|
||||||
|
note_history_id: newNoteHistoryId,
|
||||||
|
note_id: noteId,
|
||||||
|
// title and text should be decrypted now
|
||||||
|
note_title: oldNote.note_title,
|
||||||
|
note_text: oldNote.note_text,
|
||||||
|
is_protected: 0, // will be fixed in the protectNoteHistory() call
|
||||||
|
date_modified_from: oldNote.date_modified,
|
||||||
|
date_modified_to: nowStr
|
||||||
|
});
|
||||||
|
|
||||||
|
await sync_table.addNoteHistorySync(newNoteHistoryId, sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveNoteImages(noteId, noteText, sourceId) {
|
||||||
|
const existingNoteImages = await sql.getAll("SELECT * FROM notes_image WHERE note_id = ?", [noteId]);
|
||||||
|
const foundImageIds = [];
|
||||||
|
const now = utils.nowDate();
|
||||||
|
const re = /src="\/api\/images\/([a-zA-Z0-9]+)\//g;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while (match = re.exec(noteText)) {
|
||||||
|
const imageId = match[1];
|
||||||
|
const existingNoteImage = existingNoteImages.find(ni => ni.image_id === imageId);
|
||||||
|
|
||||||
|
if (!existingNoteImage) {
|
||||||
|
const noteImageId = utils.newNoteImageId();
|
||||||
|
|
||||||
|
await sql.insert("notes_image", {
|
||||||
|
note_image_id: noteImageId,
|
||||||
|
note_id: noteId,
|
||||||
|
image_id: imageId,
|
||||||
|
is_deleted: 0,
|
||||||
|
date_modified: now,
|
||||||
|
date_created: now
|
||||||
|
});
|
||||||
|
|
||||||
|
await sync_table.addNoteImageSync(noteImageId, sourceId);
|
||||||
|
}
|
||||||
|
else if (existingNoteImage.is_deleted) {
|
||||||
|
await sql.execute("UPDATE notes_image SET is_deleted = 0, date_modified = ? WHERE note_image_id = ?",
|
||||||
|
[now, existingNoteImage.note_image_id]);
|
||||||
|
|
||||||
|
await sync_table.addNoteImageSync(existingNoteImage.note_image_id, sourceId);
|
||||||
|
}
|
||||||
|
// else we don't need to do anything
|
||||||
|
|
||||||
|
foundImageIds.push(imageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// marking note images as deleted if they are not present on the page anymore
|
||||||
|
const unusedNoteImages = existingNoteImages.filter(ni => !foundImageIds.includes(ni.image_id));
|
||||||
|
|
||||||
|
for (const unusedNoteImage of unusedNoteImages) {
|
||||||
|
await sql.execute("UPDATE notes_image SET is_deleted = 1, date_modified = ? WHERE note_image_id = ?",
|
||||||
|
[now, unusedNoteImage.note_image_id]);
|
||||||
|
|
||||||
|
await sync_table.addNoteImageSync(unusedNoteImage.note_image_id, sourceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function updateNote(noteId, newNote, dataKey, sourceId) {
|
async function updateNote(noteId, newNote, dataKey, sourceId) {
|
||||||
if (newNote.detail.is_protected) {
|
if (newNote.detail.is_protected) {
|
||||||
await encryptNote(newNote, dataKey);
|
await encryptNote(newNote, dataKey);
|
||||||
@@ -156,28 +224,11 @@ async function updateNote(noteId, newNote, dataKey, sourceId) {
|
|||||||
const msSinceDateCreated = now.getTime() - utils.parseDate(newNote.detail.date_created).getTime();
|
const msSinceDateCreated = now.getTime() - utils.parseDate(newNote.detail.date_created).getTime();
|
||||||
|
|
||||||
if (!existingNoteHistoryId && msSinceDateCreated >= historySnapshotTimeInterval * 1000) {
|
if (!existingNoteHistoryId && msSinceDateCreated >= historySnapshotTimeInterval * 1000) {
|
||||||
const oldNote = await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [noteId]);
|
await saveNoteHistory(noteId, dataKey, sourceId, nowStr);
|
||||||
|
|
||||||
if (oldNote.is_protected) {
|
|
||||||
decryptNote(oldNote, dataKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newNoteHistoryId = utils.newNoteHistoryId();
|
|
||||||
|
|
||||||
await sql.insert('notes_history', {
|
|
||||||
note_history_id: newNoteHistoryId,
|
|
||||||
note_id: noteId,
|
|
||||||
// title and text should be decrypted now
|
|
||||||
note_title: oldNote.note_title,
|
|
||||||
note_text: oldNote.note_text,
|
|
||||||
is_protected: 0, // will be fixed in the protectNoteHistory() call
|
|
||||||
date_modified_from: oldNote.date_modified,
|
|
||||||
date_modified_to: nowStr
|
|
||||||
});
|
|
||||||
|
|
||||||
await sync_table.addNoteHistorySync(newNoteHistoryId, sourceId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await saveNoteImages(noteId, newNote.detail.note_text, sourceId);
|
||||||
|
|
||||||
await protectNoteHistory(noteId, dataKey, newNote.detail.is_protected);
|
await protectNoteHistory(noteId, dataKey, newNote.detail.is_protected);
|
||||||
|
|
||||||
await sql.execute("UPDATE notes SET note_title = ?, note_text = ?, is_protected = ?, date_modified = ? WHERE note_id = ?", [
|
await sql.execute("UPDATE notes SET note_title = ?, note_text = ?, is_protected = ?, date_modified = ? WHERE note_id = ?", [
|
||||||
|
|||||||
25
services/resource_dir.js
Normal file
25
services/resource_dir.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
const log = require('./log');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const RESOURCE_DIR = path.resolve(__dirname, "..");
|
||||||
|
|
||||||
|
const MIGRATIONS_DIR = path.resolve(RESOURCE_DIR, "migrations");
|
||||||
|
|
||||||
|
if (!fs.existsSync(MIGRATIONS_DIR)) {
|
||||||
|
log.error("Could not find migration directory: " + MIGRATIONS_DIR);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DB_INIT_DIR = path.resolve(RESOURCE_DIR, "db");
|
||||||
|
|
||||||
|
if (!fs.existsSync(DB_INIT_DIR)) {
|
||||||
|
log.error("Could not find DB initialization directory: " + DB_INIT_DIR);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
RESOURCE_DIR,
|
||||||
|
MIGRATIONS_DIR,
|
||||||
|
DB_INIT_DIR
|
||||||
|
};
|
||||||
@@ -4,8 +4,8 @@ const log = require('./log');
|
|||||||
const dataDir = require('./data_dir');
|
const dataDir = require('./data_dir');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const sqlite = require('sqlite');
|
const sqlite = require('sqlite');
|
||||||
const utils = require('./utils');
|
|
||||||
const app_info = require('./app_info');
|
const app_info = require('./app_info');
|
||||||
|
const resource_dir = require('./resource_dir');
|
||||||
|
|
||||||
async function createConnection() {
|
async function createConnection() {
|
||||||
return await sqlite.open(dataDir.DOCUMENT_PATH, {Promise});
|
return await sqlite.open(dataDir.DOCUMENT_PATH, {Promise});
|
||||||
@@ -28,18 +28,18 @@ const dbReady = new Promise((resolve, reject) => {
|
|||||||
if (tableResults.length !== 1) {
|
if (tableResults.length !== 1) {
|
||||||
log.info("Connected to db, but schema doesn't exist. Initializing schema ...");
|
log.info("Connected to db, but schema doesn't exist. Initializing schema ...");
|
||||||
|
|
||||||
const schema = fs.readFileSync('db/schema.sql', 'UTF-8');
|
const schema = fs.readFileSync(resource_dir.DB_INIT_DIR + '/schema.sql', 'UTF-8');
|
||||||
const notesSql = fs.readFileSync('db/main_notes.sql', 'UTF-8');
|
const notesSql = fs.readFileSync(resource_dir.DB_INIT_DIR + '/main_notes.sql', 'UTF-8');
|
||||||
const notesTreeSql = fs.readFileSync('db/main_notes_tree.sql', 'UTF-8');
|
const notesTreeSql = fs.readFileSync(resource_dir.DB_INIT_DIR + '/main_notes_tree.sql', 'UTF-8');
|
||||||
|
|
||||||
await doInTransaction(async () => {
|
await doInTransaction(async () => {
|
||||||
await executeScript(schema);
|
await executeScript(schema);
|
||||||
await executeScript(notesSql);
|
await executeScript(notesSql);
|
||||||
await executeScript(notesTreeSql);
|
await executeScript(notesTreeSql);
|
||||||
|
|
||||||
const noteId = await getFirstValue("SELECT note_id FROM notes_tree WHERE parent_note_id = 'root' ORDER BY note_position");
|
const startNoteId = await getFirstValue("SELECT note_id FROM notes_tree WHERE parent_note_id = 'root' AND is_deleted = 0 ORDER BY note_position");
|
||||||
|
|
||||||
await require('./options').initOptions(noteId);
|
await require('./options').initOptions(startNoteId);
|
||||||
await require('./sync_table').fillAllSyncRows();
|
await require('./sync_table').fillAllSyncRows();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,9 +49,7 @@ const dbReady = new Promise((resolve, reject) => {
|
|||||||
// the database
|
// the database
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const username = await getFirstValue("SELECT opt_value FROM options WHERE opt_name = 'username'");
|
if (!await isUserInitialized()) {
|
||||||
|
|
||||||
if (!username) {
|
|
||||||
log.info("Login/password not initialized. DB not ready.");
|
log.info("Login/password not initialized. DB not ready.");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -235,8 +233,15 @@ async function isDbUpToDate() {
|
|||||||
return upToDate;
|
return upToDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function isUserInitialized() {
|
||||||
|
const username = await getFirstValue("SELECT opt_value FROM options WHERE opt_name = 'username'");
|
||||||
|
|
||||||
|
return !!username;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
dbReady,
|
dbReady,
|
||||||
|
isUserInitialized,
|
||||||
insert,
|
insert,
|
||||||
replace,
|
replace,
|
||||||
getFirstValue,
|
getFirstValue,
|
||||||
|
|||||||
@@ -14,22 +14,13 @@ const fs = require('fs');
|
|||||||
const app_info = require('./app_info');
|
const app_info = require('./app_info');
|
||||||
const messaging = require('./messaging');
|
const messaging = require('./messaging');
|
||||||
const sync_setup = require('./sync_setup');
|
const sync_setup = require('./sync_setup');
|
||||||
|
const sync_mutex = require('./sync_mutex');
|
||||||
|
|
||||||
let syncInProgress = false;
|
|
||||||
let proxyToggle = true;
|
let proxyToggle = true;
|
||||||
let syncServerCertificate = null;
|
let syncServerCertificate = null;
|
||||||
|
|
||||||
async function sync() {
|
async function sync() {
|
||||||
if (syncInProgress) {
|
const releaseMutex = await sync_mutex.acquire();
|
||||||
log.info("Sync already in progress");
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "Sync already in progress"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
syncInProgress = true;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!await sql.isDbUpToDate()) {
|
if (!await sql.isDbUpToDate()) {
|
||||||
@@ -74,7 +65,7 @@ async function sync() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
syncInProgress = false;
|
releaseMutex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +143,12 @@ async function pullSync(syncContext) {
|
|||||||
else if (sync.entity_name === 'recent_notes') {
|
else if (sync.entity_name === 'recent_notes') {
|
||||||
await syncUpdate.updateRecentNotes(resp, syncContext.sourceId);
|
await syncUpdate.updateRecentNotes(resp, syncContext.sourceId);
|
||||||
}
|
}
|
||||||
|
else if (sync.entity_name === 'images') {
|
||||||
|
await syncUpdate.updateImage(resp, syncContext.sourceId);
|
||||||
|
}
|
||||||
|
else if (sync.entity_name === 'notes_image') {
|
||||||
|
await syncUpdate.updateNoteImage(resp, syncContext.sourceId);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
throw new Error(`Unrecognized entity type ${sync.entity_name} in sync #${sync.id}`);
|
throw new Error(`Unrecognized entity type ${sync.entity_name} in sync #${sync.id}`);
|
||||||
}
|
}
|
||||||
@@ -214,7 +211,7 @@ async function pushEntity(sync, syncContext) {
|
|||||||
else if (sync.entity_name === 'notes_reordering') {
|
else if (sync.entity_name === 'notes_reordering') {
|
||||||
entity = {
|
entity = {
|
||||||
parent_note_id: sync.entity_id,
|
parent_note_id: sync.entity_id,
|
||||||
ordering: await sql.getMap('SELECT note_tree_id, note_position FROM notes_tree WHERE parent_note_id = ?', [sync.entity_id])
|
ordering: await sql.getMap('SELECT note_tree_id, note_position FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [sync.entity_id])
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if (sync.entity_name === 'options') {
|
else if (sync.entity_name === 'options') {
|
||||||
@@ -223,6 +220,16 @@ async function pushEntity(sync, syncContext) {
|
|||||||
else if (sync.entity_name === 'recent_notes') {
|
else if (sync.entity_name === 'recent_notes') {
|
||||||
entity = await sql.getFirst('SELECT * FROM recent_notes WHERE note_tree_id = ?', [sync.entity_id]);
|
entity = await sql.getFirst('SELECT * FROM recent_notes WHERE note_tree_id = ?', [sync.entity_id]);
|
||||||
}
|
}
|
||||||
|
else if (sync.entity_name === 'images') {
|
||||||
|
entity = await sql.getFirst('SELECT * FROM images WHERE image_id = ?', [sync.entity_id]);
|
||||||
|
|
||||||
|
if (entity.data !== null) {
|
||||||
|
entity.data = entity.data.toString('base64');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (sync.entity_name === 'notes_image') {
|
||||||
|
entity = await sql.getFirst('SELECT * FROM notes_image WHERE note_image_id = ?', [sync.entity_id]);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
throw new Error(`Unrecognized entity type ${sync.entity_name} in sync #${sync.id}`);
|
throw new Error(`Unrecognized entity type ${sync.entity_name} in sync #${sync.id}`);
|
||||||
}
|
}
|
||||||
|
|||||||
8
services/sync_mutex.js
Normal file
8
services/sync_mutex.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Sync makes process can make data intermittently inconsistent. Processes which require strong data consistency
|
||||||
|
* (like consistency checks) can use this mutex to make sure sync isn't currently running.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Mutex = require('async-mutex').Mutex;
|
||||||
|
|
||||||
|
module.exports = new Mutex();
|
||||||
@@ -24,8 +24,16 @@ async function addOptionsSync(optName, sourceId) {
|
|||||||
await addEntitySync("options", optName, sourceId);
|
await addEntitySync("options", optName, sourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addRecentNoteSync(notePath, sourceId) {
|
async function addRecentNoteSync(noteTreeId, sourceId) {
|
||||||
await addEntitySync("recent_notes", notePath, sourceId);
|
await addEntitySync("recent_notes", noteTreeId, sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addImageSync(imageId, sourceId) {
|
||||||
|
await addEntitySync("images", imageId, sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addNoteImageSync(noteImageId, sourceId) {
|
||||||
|
await addEntitySync("notes_image", noteImageId, sourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addEntitySync(entityName, entityId, sourceId) {
|
async function addEntitySync(entityName, entityId, sourceId) {
|
||||||
@@ -78,6 +86,8 @@ async function fillAllSyncRows() {
|
|||||||
await fillSyncRows("notes_tree", "note_tree_id");
|
await fillSyncRows("notes_tree", "note_tree_id");
|
||||||
await fillSyncRows("notes_history", "note_history_id");
|
await fillSyncRows("notes_history", "note_history_id");
|
||||||
await fillSyncRows("recent_notes", "note_tree_id");
|
await fillSyncRows("recent_notes", "note_tree_id");
|
||||||
|
await fillSyncRows("images", "image_id");
|
||||||
|
await fillSyncRows("notes_image", "note_image_id");
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -87,6 +97,8 @@ module.exports = {
|
|||||||
addNoteHistorySync,
|
addNoteHistorySync,
|
||||||
addOptionsSync,
|
addOptionsSync,
|
||||||
addRecentNoteSync,
|
addRecentNoteSync,
|
||||||
|
addImageSync,
|
||||||
|
addNoteImageSync,
|
||||||
cleanupSyncRowsForMissingEntities,
|
cleanupSyncRowsForMissingEntities,
|
||||||
fillAllSyncRows
|
fillAllSyncRows
|
||||||
};
|
};
|
||||||
@@ -40,7 +40,9 @@ async function updateNoteHistory(entity, sourceId) {
|
|||||||
const orig = await sql.getFirstOrNull("SELECT * FROM notes_history WHERE note_history_id = ?", [entity.note_history_id]);
|
const orig = await sql.getFirstOrNull("SELECT * FROM notes_history WHERE note_history_id = ?", [entity.note_history_id]);
|
||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
if (orig === null || orig.date_modified_to < entity.date_modified_to) {
|
// we update note history even if date modified to is the same because the only thing which might have changed
|
||||||
|
// is the protected status (and correnspondingly note_title and note_text) which doesn't affect the date_modified_to
|
||||||
|
if (orig === null || orig.date_modified_to <= entity.date_modified_to) {
|
||||||
await sql.replace('notes_history', entity);
|
await sql.replace('notes_history', entity);
|
||||||
|
|
||||||
await sync_table.addNoteHistorySync(entity.note_history_id, sourceId);
|
await sync_table.addNoteHistorySync(entity.note_history_id, sourceId);
|
||||||
@@ -90,11 +92,45 @@ async function updateRecentNotes(entity, sourceId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateImage(entity, sourceId) {
|
||||||
|
if (entity.data !== null) {
|
||||||
|
entity.data = Buffer.from(entity.data, 'base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
const origImage = await sql.getFirst("SELECT * FROM images WHERE image_id = ?", [entity.image_id]);
|
||||||
|
|
||||||
|
if (!origImage || origImage.date_modified <= entity.date_modified) {
|
||||||
|
await sql.doInTransaction(async () => {
|
||||||
|
await sql.replace("images", entity);
|
||||||
|
|
||||||
|
await sync_table.addImageSync(entity.image_id, sourceId);
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("Update/sync image " + entity.image_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateNoteImage(entity, sourceId) {
|
||||||
|
const origNoteImage = await sql.getFirst("SELECT * FROM notes_image WHERE note_image_id = ?", [entity.note_image_id]);
|
||||||
|
|
||||||
|
if (!origNoteImage || origNoteImage.date_modified <= entity.date_modified) {
|
||||||
|
await sql.doInTransaction(async () => {
|
||||||
|
await sql.replace("notes_image", entity);
|
||||||
|
|
||||||
|
await sync_table.addNoteImageSync(entity.note_image_id, sourceId);
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("Update/sync note image " + entity.note_image_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
updateNote,
|
updateNote,
|
||||||
updateNoteTree,
|
updateNoteTree,
|
||||||
updateNoteHistory,
|
updateNoteHistory,
|
||||||
updateNoteReordering,
|
updateNoteReordering,
|
||||||
updateOptions,
|
updateOptions,
|
||||||
updateRecentNotes
|
updateRecentNotes,
|
||||||
|
updateImage,
|
||||||
|
updateNoteImage
|
||||||
};
|
};
|
||||||
@@ -15,6 +15,14 @@ function newNoteHistoryId() {
|
|||||||
return randomString(12);
|
return randomString(12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function newImageId() {
|
||||||
|
return randomString(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
function newNoteImageId() {
|
||||||
|
return randomString(12);
|
||||||
|
}
|
||||||
|
|
||||||
function randomString(length) {
|
function randomString(length) {
|
||||||
return randtoken.generate(length);
|
return randtoken.generate(length);
|
||||||
}
|
}
|
||||||
@@ -79,6 +87,14 @@ function sanitizeSql(str) {
|
|||||||
return str.replace(/'/g, "\\'");
|
return str.replace(/'/g, "\\'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertArguments() {
|
||||||
|
for (const i in arguments) {
|
||||||
|
if (!arguments[i]) {
|
||||||
|
throw new Error(`Argument idx#${i} should not be falsy: ${arguments[i]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
randomSecureToken,
|
randomSecureToken,
|
||||||
randomString,
|
randomString,
|
||||||
@@ -88,6 +104,8 @@ module.exports = {
|
|||||||
newNoteId,
|
newNoteId,
|
||||||
newNoteTreeId,
|
newNoteTreeId,
|
||||||
newNoteHistoryId,
|
newNoteHistoryId,
|
||||||
|
newImageId,
|
||||||
|
newNoteImageId,
|
||||||
toBase64,
|
toBase64,
|
||||||
fromBase64,
|
fromBase64,
|
||||||
hmac,
|
hmac,
|
||||||
@@ -95,5 +113,6 @@ module.exports = {
|
|||||||
hash,
|
hash,
|
||||||
isEmptyOrWhitespace,
|
isEmptyOrWhitespace,
|
||||||
getDateTimeForFile,
|
getDateTimeForFile,
|
||||||
sanitizeSql
|
sanitizeSql,
|
||||||
|
assertArguments
|
||||||
};
|
};
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
echo 'module.exports = { build_date:"'`date --iso-8601=seconds`'", build_revision: "'`git log -1 --format="%H"`'" };' > services/build.js
|
|
||||||
@@ -2,22 +2,20 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Trilium</title>
|
<title>Trilium Notes</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="loader-wrapper"><div id="loader"></div></div>
|
<div id="container" style="display:none;">
|
||||||
|
|
||||||
<div id="container" style="display: none;">
|
|
||||||
<div id="header" class="hide-toggle">
|
<div id="header" class="hide-toggle">
|
||||||
<div id="header-title">
|
<div id="header-title">
|
||||||
<img src="images/app-icons/png/24x24.png">
|
<img src="images/app-icons/png/24x24.png">
|
||||||
|
|
||||||
Trilium
|
Trilium Notes
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="flex-grow: 100;">
|
<div style="flex-grow: 100;">
|
||||||
<button class="btn btn-xs" onclick="jumpToNote.showDialog();">Jump to note</button>
|
<button class="btn btn-xs" onclick="jumpToNote.showDialog();" title="CTRL+J">Jump to note</button>
|
||||||
<button class="btn btn-xs" onclick="recentNotes.showDialog();">Recent notes</button>
|
<button class="btn btn-xs" onclick="recentNotes.showDialog();" title="CTRL+E">Recent notes</button>
|
||||||
<button class="btn btn-xs" onclick="recentChanges.showDialog();">Recent changes</button>
|
<button class="btn btn-xs" onclick="recentChanges.showDialog();">Recent changes</button>
|
||||||
<button class="btn btn-xs" onclick="eventLog.showDialog();">Event log</button>
|
<button class="btn btn-xs" onclick="eventLog.showDialog();">Event log</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,7 +27,7 @@
|
|||||||
|
|
||||||
<button class="btn btn-xs" onclick="settings.showDialog();">Settings</button>
|
<button class="btn btn-xs" onclick="settings.showDialog();">Settings</button>
|
||||||
|
|
||||||
<form action="logout" method="POST" style="display: inline;">
|
<form action="logout" id="logout-button" method="POST" style="display: inline;">
|
||||||
<input type="submit" class="btn btn-xs" value="Logout">
|
<input type="submit" class="btn btn-xs" value="Logout">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,7 +43,7 @@
|
|||||||
<img src="images/icons/list.png" alt="Collapse note tree"/>
|
<img src="images/icons/list.png" alt="Collapse note tree"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a onclick="noteTree.scrollToCurrentNote()" title="Scroll to current note" class="icon-action">
|
<a onclick="noteTree.scrollToCurrentNote()" title="Scroll to current note. Shortcut CTRL+." class="icon-action">
|
||||||
<img src="images/icons/crosshair.png" alt="Scroll to current note"/>
|
<img src="images/icons/crosshair.png" alt="Scroll to current note"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -67,7 +65,7 @@
|
|||||||
<div id="tree" class="hide-toggle" style="grid-area: tree; overflow: auto;">
|
<div id="tree" class="hide-toggle" style="grid-area: tree; overflow: auto;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="parent-list">
|
<div id="parent-list" class="hide-toggle">
|
||||||
<p><strong>Note locations:</strong></p>
|
<p><strong>Note locations:</strong></p>
|
||||||
|
|
||||||
<ul id="parent-list-list"></ul>
|
<ul id="parent-list-list"></ul>
|
||||||
@@ -95,7 +93,9 @@
|
|||||||
|
|
||||||
<input autocomplete="off" value="" id="note-title" style="font-size: x-large; border: 0; flex-grow: 100;" tabindex="1">
|
<input autocomplete="off" value="" id="note-title" style="font-size: x-large; border: 0; flex-grow: 100;" tabindex="1">
|
||||||
|
|
||||||
<button class="btn btn-xs" style="margin: 10px;" onclick="noteHistory.showCurrentNoteHistory();">Note history</button>
|
<span id="note-id-display" title="Note ID"></span>
|
||||||
|
|
||||||
|
<button class="btn btn-xs" title="ALT+H" style="margin: 10px;" onclick="noteHistory.showCurrentNoteHistory();">Note history</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -111,13 +111,13 @@
|
|||||||
<br/><br/>
|
<br/><br/>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<button class="btn btn-sm" id="recent-notes-jump-to">Jump to (enter)</button>
|
<button class="btn btn-sm" id="recent-notes-jump-to">Jump to <kbd>enter</kbd></button>
|
||||||
|
|
||||||
<button class="btn btn-sm" id="recent-notes-add-link">Add link (l)</button>
|
<button class="btn btn-sm" id="recent-notes-add-link">Add link <kbd>l</kbd></button>
|
||||||
|
|
||||||
<button class="btn btn-sm" id="recent-notes-add-current-as-child">Add current as child (c)</button>
|
<button class="btn btn-sm" id="recent-notes-add-current-as-child">Add current as child <kbd>c</kbd></button>
|
||||||
|
|
||||||
<button class="btn btn-sm" id="recent-notes-add-recent-as-child">Add recent as child (r)</button>
|
<button class="btn btn-sm" id="recent-notes-add-recent-as-child">Add recent as child <kbd>r</kbd></button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -163,17 +163,18 @@
|
|||||||
<input id="jump-to-note-autocomplete" style="width: 100%;">
|
<input id="jump-to-note-autocomplete" style="width: 100%;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button name="action" value="jump" class="btn btn-sm">Jump</button>
|
<button name="action" value="jump" class="btn btn-sm">Jump <kbd>enter</kbd></button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="protected-session-password-dialog" title="Protected session" style="display: none;">
|
<div id="protected-session-password-dialog" title="Protected session" style="display: none;">
|
||||||
<form id="protected-session-password-form">
|
<form id="protected-session-password-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="protected-session-password">To proceed with requested action you need to enter protected session by entering password:</label>
|
<label for="protected-session-password">To proceed with requested action you need to start protected session by entering password:</label>
|
||||||
<input id="protected-session-password" style="width: 250px;" type="password">
|
<input id="protected-session-password" class="form-control" type="password">
|
||||||
<button class="btn btn-sm">Show</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-sm">Start protected session <kbd>enter</kbd></button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -315,7 +316,7 @@
|
|||||||
|
|
||||||
<div id="sql-console-dialog" title="SQL console" style="display: none; padding: 20px;">
|
<div id="sql-console-dialog" title="SQL console" style="display: none; padding: 20px;">
|
||||||
<textarea style="width: 100%; height: 100px" id="sql-console-query"></textarea>
|
<textarea style="width: 100%; height: 100px" id="sql-console-query"></textarea>
|
||||||
<button class="btn btn-danger" id="sql-console-execute">Execute</button>
|
<button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button>
|
||||||
|
|
||||||
<table id="sql-console-results" class="table table-striped" style="overflow: scroll; width: 100%;">
|
<table id="sql-console-results" class="table table-striped" style="overflow: scroll; width: 100%;">
|
||||||
<thead></thead>
|
<thead></thead>
|
||||||
@@ -323,6 +324,10 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="note-source-dialog" title="Note source" style="display: none; padding: 20px;">
|
||||||
|
<textarea id="note-source" readonly="readonly"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="tooltip" style="display: none;"></div>
|
<div id="tooltip" style="display: none;"></div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
@@ -387,9 +392,16 @@
|
|||||||
<script src="javascripts/dialogs/event_log.js"></script>
|
<script src="javascripts/dialogs/event_log.js"></script>
|
||||||
<script src="javascripts/dialogs/edit_tree_prefix.js"></script>
|
<script src="javascripts/dialogs/edit_tree_prefix.js"></script>
|
||||||
<script src="javascripts/dialogs/sql_console.js"></script>
|
<script src="javascripts/dialogs/sql_console.js"></script>
|
||||||
|
<script src="javascripts/dialogs/note_source.js"></script>
|
||||||
|
|
||||||
<script src="javascripts/link.js"></script>
|
<script src="javascripts/link.js"></script>
|
||||||
<script src="javascripts/sync.js"></script>
|
<script src="javascripts/sync.js"></script>
|
||||||
<script src="javascripts/messaging.js"></script>
|
<script src="javascripts/messaging.js"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
// we hide container initally because otherwise it is rendered first without CSS and then flickers into
|
||||||
|
// final form which is pretty ugly.
|
||||||
|
$("#container").show();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -48,6 +48,5 @@
|
|||||||
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
|
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
|
||||||
|
|
||||||
<link href="libraries/bootstrap/css/bootstrap.css" rel="stylesheet">
|
<link href="libraries/bootstrap/css/bootstrap.css" rel="stylesheet">
|
||||||
<script src="libraries/bootstrap/js/bootstrap.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div style="width: 500px; margin: auto;">
|
<div style="width: 500px; margin: auto;">
|
||||||
<h1>Trilium setup</h1>
|
<h1>Trilium Notes setup</h1>
|
||||||
|
|
||||||
<div class="alert alert-warning" id="alert" style="display: none;">
|
<div class="alert alert-warning" id="alert" style="display: none;">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user