Compare commits

...

55 Commits

Author SHA1 Message Date
azivner
361d8a4216 release 0.2.0 2018-01-01 23:29:34 -05:00
azivner
ae6e222c50 fix for moving note inside not-yet folder 2018-01-01 23:28:00 -05:00
azivner
37995f1ce5 info message about copying / cutting into clipboard 2018-01-01 22:33:21 -05:00
azivner
ad7fa5e096 better conflict detection 2018-01-01 22:28:19 -05:00
azivner
3585982758 measuring and logging time to compute content hash and consistency checks 2018-01-01 19:47:50 -05:00
azivner
c776f298f2 tree cycle consistency check 2018-01-01 19:41:22 -05:00
azivner
f07c427da1 fixed race condition when changing note path in URL hash which caused flickering 2018-01-01 18:53:52 -05:00
azivner
e560072f8b drag & drop support for multi select plus some fixes 2018-01-01 18:29:06 -05:00
azivner
3f976a3821 ctrl-a selects all nodes at the current level (without children) 2018-01-01 18:11:23 -05:00
azivner
274bb32696 multi-select in note tree and clipboard operations on the selection 2018-01-01 17:59:59 -05:00
azivner
99b163a042 fix for forced note sync 2017-12-30 21:55:44 -05:00
azivner
fdcc833f6d added force note sync functionality to context menu 2017-12-30 21:44:26 -05:00
azivner
a8e45019e4 failing early when decryption fails 2017-12-30 20:14:05 -05:00
azivner
7f7028873c fixed synced issue when changing protected status of note history 2017-12-30 19:38:30 -05:00
azivner
2d2d76a715 enter on note tree can also give focus to the editor 2017-12-28 22:57:35 -05:00
azivner
69cbfaae17 release 0.1.2 2017-12-28 21:17:25 -05:00
azivner
aebce8f12b making not focused active tree item more visible 2017-12-28 20:53:31 -05:00
azivner
045ca1f0bf global CTRL-SHIFT-arrows to move in the note tree without losing focus in the note editor 2017-12-28 20:38:57 -05:00
azivner
bf2db6eac7 after loading new note make sure editor is scrolled to the top 2017-12-28 20:13:54 -05:00
azivner
cf84114f91 removed unused methods 2017-12-28 19:58:33 -05:00
azivner
6426157bb3 title in fancytree needs to be escaped for HTML special characters 2017-12-28 19:00:31 -05:00
azivner
332fc16852 fix pasting into non-expanded non-loaded node (UI only) 2017-12-27 22:44:27 -05:00
azivner
da2cd57428 CTRL+ENTER on note in tree pane switches to the editor 2017-12-27 21:16:47 -05:00
azivner
de9bab1181 fixed keyboard controlled clipboard 2017-12-27 21:12:54 -05:00
azivner
136375cf11 refactored detection of whether user is initialized 2017-12-27 18:23:24 -05:00
azivner
eabc7f80b7 release 0.1.1 2017-12-27 17:41:07 -05:00
azivner
6405d6e066 added linux ia32 build 2017-12-27 17:39:41 -05:00
azivner
f6d481a9e2 fix resource path bug 2017-12-27 16:44:15 -05:00
azivner
695f0e5879 release 0.1.0 2017-12-26 22:59:46 -05:00
azivner
ae337e4500 don't open dev tools for search page in electron 2017-12-26 22:24:47 -05:00
azivner
19ffa14f10 hide logout button in electron since it doesn't do anything 2017-12-26 22:19:42 -05:00
azivner
bf3f26fde8 using textarea instead of pre for note source since that's easier to copy to clipboard 2017-12-26 20:54:41 -05:00
azivner
dece400207 release 0.0.11 2017-12-26 19:57:44 -05:00
azivner
baab745462 some visual tweaks 2017-12-26 19:54:43 -05:00
azivner
0d3b3ec7c5 alt+s toggles search instead of just showing it 2017-12-26 19:16:04 -05:00
azivner
7aff20bb0d added show note source 2017-12-26 18:15:29 -05:00
azivner
5acf84aece Calling project "Trilium Notes" in some places as it is more self explanatory 2017-12-26 12:55:40 -05:00
azivner
c58a0df76d added list of wiki pages into README 2017-12-26 11:08:23 -05:00
azivner
20c14a1920 fix collapse tree keyboard shortcut 2017-12-26 11:00:04 -05:00
azivner
04063d8a9c sql console reports successful execution (useful when query doesn't produce any result set like in update/delete) 2017-12-26 10:21:33 -05:00
azivner
dd69e0135b added collapse sub-tree in context menu 2017-12-26 10:00:08 -05:00
azivner
ab6e78f726 keyboard shortcut titles on buttons 2017-12-26 09:44:16 -05:00
azivner
9029d18178 fix go to external link by double clicking 2017-12-26 09:10:54 -05:00
azivner
e9a1791e3d fix unwanted movement with navigation keys 2017-12-25 21:47:32 -05:00
azivner
70e13c8a20 release 0.0.10 2017-12-25 21:06:39 -05:00
azivner
7e1cc729f9 have to add changed files to index before committing 2017-12-25 21:06:35 -05:00
azivner
87e7828440 release scripts fixes 2017-12-25 21:05:08 -05:00
azivner
29fd78aee5 fix version setting in release script 2017-12-25 20:45:59 -05:00
azivner
f90c2317fc release script, moved all scripts to bin directory 2017-12-25 15:01:33 -05:00
azivner
bca5087426 fix links 2017-12-25 12:50:16 -05:00
azivner
583123ab0a classes in extraClasses need to be separated by space, not comma 2017-12-25 09:59:48 -05:00
azivner
12f70b28c8 display current note ID in editor (useful in Electron which doesn't have address bar) 2017-12-25 09:46:11 -05:00
azivner
31a4a201a8 fix setting "protected" class and background on note tree 2017-12-25 09:30:37 -05:00
azivner
5ec866aa50 fixed links in readme 2017-12-25 08:37:40 -05:00
azivner
cdede3240c updated build script to package both linux and windows distribution packages with xz and 7zip 2017-12-24 23:14:19 -05:00
46 changed files with 803 additions and 281 deletions

View File

@@ -1,12 +1,12 @@
# Trilium # Trilium Notes
Hierarchical note taking application. Trilium Notes is a hierarchical note taking application. Picture tells a thousand words:
![](https://raw.githubusercontent.com/wiki/zadam/trilium/images/screenshot.png) ![](https://raw.githubusercontent.com/wiki/zadam/trilium/images/screenshot.png)
## 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)

View File

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

View File

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

View File

@@ -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>&nbsp;</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>&nbsp;</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>&nbsp;</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>Welcome to Trilium Notes!</p><p>&nbsp;</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>&nbsp;</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>&nbsp;</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 ('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>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</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>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</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');

View File

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

View File

@@ -1,7 +1,7 @@
{ {
"name": "trilium", "name": "trilium",
"description": "Trilium", "description": "Trilium Notes",
"version": "0.0.9", "version": "0.2.0",
"scripts": { "scripts": {
"start": "node ./bin/www", "start": "node ./bin/www",
"test-electron": "xo", "test-electron": "xo",

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -49,7 +49,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 +57,42 @@ $(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;
});
$(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
@@ -164,4 +200,6 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
messaging.logError(message); messaging.logError(message);
return false; return false;
}; };
$("#logout-button").toggle(!isElectron());

View File

@@ -42,18 +42,20 @@ 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')); const address = linkEl.attr("note-path") ? linkEl.attr("note-path") : linkEl.attr('href');
if (!notePath) { if (!address) {
return; return;
} }
if (notePath.startsWith('http')) { if (address.startsWith('http')) {
window.open(notePath, '_blank'); window.open(address, '_blank');
return; return;
} }
const notePath = getNotePathFromLink(address);
noteTree.activateNode(notePath); noteTree.activateNode(notePath);
// this is quite ugly hack, but it seems like we can't close the tooltip otherwise // this is quite ugly hack, but it seems like we can't close the tooltip otherwise
@@ -84,8 +86,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,

View File

@@ -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,6 +126,10 @@ const noteEditor = (function() {
noteChangeDisabled = false; noteChangeDisabled = false;
setNoteBackgroundIfProtected(currentNote); setNoteBackgroundIfProtected(currentNote);
noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
// after loading new note make sure editor is scrolled to the top
noteDetailWrapperEl.scrollTop(0);
showAppIfHidden(); showAppIfHidden();
} }

View File

@@ -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,119 @@ 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;
},
"ctrl+return": node => {
noteDetailEl.focus();
},
"return": node => {
noteDetailEl.focus();
},
// 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 +527,22 @@ 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') {
node.setActive();
if (!event.ctrlKey) {
clearSelectedNodes();
}
node.setSelected(true);
return false;
}
},
activate: (event, data) => { activate: (event, data) => {
const node = data.node.data; const node = data.node.data;
@@ -506,57 +593,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 +635,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 +657,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 +801,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 +826,8 @@ const noteTree = (function() {
reload, reload,
collapseTree, collapseTree,
scrollToCurrentNote, scrollToCurrentNote,
setCurrentNoteTreeBasedOnProtectedStatus, setNoteTreeBackgroundBasedOnProtectedStatus,
setProtected,
getCurrentNode, getCurrentNode,
activateNode, activateNode,
getCurrentNotePath, getCurrentNotePath,
@@ -789,6 +840,7 @@ const noteTree = (function() {
setPrefix, setPrefix,
getNotePathTitle, getNotePathTitle,
removeParentChildRelation, removeParentChildRelation,
setParentChildRelation setParentChildRelation,
getSelectedNodes
}; };
})(); })();

View File

@@ -115,6 +115,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 +131,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);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,15 +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;
} }
.ui-menu kbd {
margin-left: 30px;
float: right;
}
#note-id-display {
color: lightgrey;
margin-left: 10px;
}
#note-source {
height: 98%;
width: 100%;
overflow: scroll;
}
#loader-wrapper{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1000;background-color:#fff;opacity:1;transition:opacity 2s ease} #loader-wrapper{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1000;background-color:#fff;opacity:1;transition:opacity 2s ease}
#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} #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}
#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} #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}

View File

@@ -67,7 +67,7 @@ router.delete('/:noteTreeId', auth.checkApiAuth, async (req, res, next) => {
router.get('/', auth.checkApiAuth, async (req, res, next) => { router.get('/', auth.checkApiAuth, 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 = [];

View File

@@ -12,6 +12,15 @@ router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, async (req,
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 sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
if (!await checkTreeCycle(parentNoteId, noteToMove.note_id)) {
return res.send({
success: false,
message: 'Moving 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]);
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 1; const newNotePos = maxNotePos === null ? 0 : maxNotePos + 1;
@@ -24,7 +33,7 @@ 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, async (req, res, next) => {
@@ -32,8 +41,16 @@ router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, asyn
const beforeNoteTreeId = req.params.beforeNoteTreeId; const beforeNoteTreeId = req.params.beforeNoteTreeId;
const sourceId = req.headers.source_id; const sourceId = req.headers.source_id;
const noteToMove = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
const beforeNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [beforeNoteTreeId]); const beforeNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [beforeNoteTreeId]);
if (!await checkTreeCycle(beforeNote.parent_note_id, noteToMove.note_id)) {
return res.send({
success: false,
message: 'Moving note here would create cycle.'
});
}
if (beforeNote) { if (beforeNote) {
await sql.doInTransaction(async () => { await sql.doInTransaction(async () => {
// we don't change date_modified so other changes are prioritized in case of conflict // we don't change date_modified so other changes are prioritized in case of conflict
@@ -51,7 +68,7 @@ router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, asyn
await sync_table.addNoteTreeSync(noteTreeId, sourceId); await sync_table.addNoteTreeSync(noteTreeId, sourceId);
}); });
res.send({}); res.send({ success: true });
} }
else { else {
res.status(500).send("Before note " + beforeNoteTreeId + " doesn't exist."); res.status(500).send("Before note " + beforeNoteTreeId + " doesn't exist.");
@@ -63,8 +80,16 @@ router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, async
const afterNoteTreeId = req.params.afterNoteTreeId; const afterNoteTreeId = req.params.afterNoteTreeId;
const sourceId = req.headers.source_id; const sourceId = req.headers.source_id;
const noteToMove = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
const afterNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [afterNoteTreeId]); const afterNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [afterNoteTreeId]);
if (!await checkTreeCycle(afterNote.parent_note_id, noteToMove.note_id)) {
return res.send({
success: false,
message: 'Moving note here would create cycle.'
});
}
if (afterNote) { if (afterNote) {
await sql.doInTransaction(async () => { await sql.doInTransaction(async () => {
// we don't change date_modified so other changes are prioritized in case of conflict // we don't change date_modified so other changes are prioritized in case of conflict
@@ -80,7 +105,7 @@ router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, async
await sync_table.addNoteTreeSync(noteTreeId, sourceId); await sync_table.addNoteTreeSync(noteTreeId, sourceId);
}); });
res.send({}); res.send({ success: true });
} }
else { else {
res.status(500).send("After note " + afterNoteTreeId + " doesn't exist."); res.status(500).send("After note " + afterNoteTreeId + " doesn't exist.");
@@ -102,7 +127,7 @@ router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, async (req
}); });
} }
if (!await checkCycle(parentNoteId, childNoteId)) { if (!await checkTreeCycle(parentNoteId, childNoteId)) {
return res.send({ return res.send({
success: false, success: false,
message: 'Cloning note here would create cycle.' message: 'Cloning note here would create cycle.'
@@ -131,9 +156,7 @@ 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, async (req, res, next) => {
@@ -147,7 +170,7 @@ router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, async (re
return res.status(500).send("After note " + afterNoteTreeId + " doesn't exist."); return res.status(500).send("After note " + afterNoteTreeId + " doesn't exist.");
} }
if (!await checkCycle(afterNote.parent_note_id, noteId)) { if (!await checkTreeCycle(afterNote.parent_note_id, noteId)) {
return res.send({ return res.send({
success: false, success: false,
message: 'Cloning note here would create cycle.' message: 'Cloning note here would create cycle.'
@@ -186,29 +209,51 @@ 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);
const children = await sql.getFirstColumn("SELECT note_id FROM notes_tree WHERE parent_note_id = ?", [parentNoteId]);
for (const childNoteId of children) {
await loadSubTreeNoteIds(childNoteId, subTreeNoteIds);
}
}
/**
* 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 = ?", [parentNoteId]);
for (const pid of parentNoteIds) {
if (!await checkTreeCycleInner(pid)) {
return false;
}
}
return true; return true;
} }
if (parentNoteId === childNoteId) { return await checkTreeCycleInner(parentNoteId);
return false;
}
const parentNoteIds = await sql.getFirstColumn("SELECT DISTINCT parent_note_id FROM notes_tree WHERE note_id = ?", [parentNoteId]);
for (const pid of parentNoteIds) {
if (!await checkCycle(pid, childNoteId)) {
return false;
}
}
return true;
} }
router.put('/:noteTreeId/expanded/:expanded', auth.checkApiAuth, async (req, res, next) => { router.put('/:noteTreeId/expanded/:expanded', auth.checkApiAuth, async (req, res, next) => {

View File

@@ -46,6 +46,30 @@ router.post('/force-full-sync', auth.checkApiAuth, async (req, res, next) => {
res.send({}); res.send({});
}); });
router.post('/force-note-sync/:noteId', auth.checkApiAuth, 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, async (req, res, next) => { router.get('/changed', auth.checkApiAuth, async (req, res, next) => {
const lastSyncId = parseInt(req.query.lastSyncId); const lastSyncId = parseInt(req.query.lastSyncId);

View File

@@ -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()) {
@@ -53,9 +50,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 {

View File

@@ -1 +1 @@
module.exports = { build_date:"2017-12-23T14:02:07-05:00", build_revision: "51215cba1bd2da8a539e86dbd31004cf9adb3f93" }; module.exports = { build_date:"2018-01-01T23:29:34-05:00", build_revision: "ae6e222c506c170ecd24d758328e0678f158bb47" };

View File

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

View File

@@ -17,6 +17,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");
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
@@ -43,6 +83,8 @@ async function runSyncRowChecks(table, key, errorList) {
async function runChecks() { async function runChecks() {
const errorList = []; const errorList = [];
const startTime = new Date();
await runCheck(` await runCheck(`
SELECT SELECT
note_id note_id
@@ -124,11 +166,21 @@ async function runChecks() {
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);
if (errorList.length === 0) {
// we run this only if basic checks passed since this assumes basic data consistency
await checkTreeCycles(errorList);
}
const elapsedTimeMs = new Date().getTime() - startTime.getTime();
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)`);
} }
} }

View File

@@ -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,9 +14,11 @@ 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(`SELECT
note_id, note_id,
note_title, note_title,
@@ -62,6 +65,12 @@ async function getHashes() {
WHERE opt_name IN (${optionsQuestionMarks}) WHERE opt_name IN (${optionsQuestionMarks})
ORDER BY opt_name`, options.SYNCED_OPTIONS)) ORDER BY opt_name`, options.SYNCED_OPTIONS))
}; };
const elapseTimeMs = new Date().getTime() - startTime.getTime();
log.info(`Content hash computation took ${elapseTimeMs}ms`);
return hashes;
} }
module.exports = { module.exports = {

View File

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

View File

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

View File

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

View File

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

25
services/resource_dir.js Normal file
View 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
};

View File

@@ -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,9 +28,9 @@ 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);
@@ -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,

View File

@@ -24,8 +24,8 @@ 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 addEntitySync(entityName, entityId, sourceId) { async function addEntitySync(entityName, entityId, sourceId) {

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
<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="loader-wrapper"><div id="loader"></div></div>
@@ -12,12 +12,12 @@
<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 +29,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 +45,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>
@@ -95,7 +95,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 +113,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>
&nbsp; &nbsp;
<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 +165,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 +318,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 +326,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,6 +394,7 @@
<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>

View File

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

View File

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