Compare commits

...

32 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
32 changed files with 545 additions and 209 deletions

View File

@@ -8,9 +8,15 @@ cp -r ../trilium-node-binaries/sqlite/* node_modules/sqlite3/lib/binding/
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
cp ../trilium-node-binaries/scrypt.node ./dist/trilium-win32-x64/resources/app/node_modules/scrypt/build/Release/

View File

@@ -4,8 +4,11 @@ VERSION=`jq -r ".version" package.json`
cd dist
echo "Packaging windows electron distribution..."
7z a trilium-windows-${VERSION}.7z trilium-win32-x64
echo "Packaging linux x64 electron distribution..."
7z a trilium-linux-x64-${VERSION}.7z trilium-linux-x64
echo "Packaging linux electron distribution..."
7z a trilium-linux-${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

View File

@@ -44,8 +44,9 @@ bin/build.sh
bin/package.sh
LINUX_BUILD=trilium-linux-$VERSION.7z
WINDOWS_BUILD=trilium-windows-$VERSION.7z
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"
@@ -53,18 +54,25 @@ github-release release \
--tag $TAG \
--name "$TAG release"
echo "Uploading linux build"
echo "Uploading linux x64 build"
github-release upload \
--tag $TAG \
--name "$LINUX_BUILD" \
--file "dist/$LINUX_BUILD"
--name "$LINUX_X64_BUILD" \
--file "dist/$LINUX_X64_BUILD"
echo "Uploading windows build"
echo "Uploading linux ia32 build"
github-release upload \
--tag $TAG \
--name "$WINDOWS_BUILD" \
--file "dist/$WINDOWS_BUILD"
--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

@@ -21,6 +21,7 @@ function createMainWindow() {
const win = new electron.BrowserWindow({
width: 1200,
height: 900,
title: 'Trilium Notes',
icon: path.join(__dirname, 'public/images/app-icons/png/256x256.png')
});

View File

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

View File

@@ -3,54 +3,72 @@
const contextMenu = (function() {
const treeEl = $("#tree");
let clipboardId = null;
let clipboardIds = [];
let clipboardMode = null;
function pasteAfter(node) {
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') {
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
}
else {
throwError("Unrecognized clipboard mode=" + clipboardMode);
}
clipboardId = null;
clipboardMode = null;
}
function pasteInto(node) {
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') {
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 {
throwError("Unrecognized clipboard mode=" + mode);
}
clipboardId = null;
clipboardMode = null;
}
function copy(node) {
clipboardId = node.data.note_id;
function copy(nodes) {
clipboardIds = nodes.map(node => node.data.note_id);
clipboardMode = 'copy';
showMessage("Note(s) have been copied into clipboard.");
}
function cut(node) {
clipboardId = node.key;
function cut(nodes) {
clipboardIds = nodes.map(node => node.key);
clipboardMode = 'cut';
showMessage("Note(s) have been cut into clipboard.");
}
const contextMenuSettings = {
@@ -71,13 +89,14 @@ const contextMenu = (function() {
{title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", 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: "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) => {
const node = $.ui.fancytree.getNode(ui.target);
// Modify menu entries depending on node status
treeEl.contextmenu("enableEntry", "pasteAfter", clipboardId !== null);
treeEl.contextmenu("enableEntry", "pasteInto", clipboardId !== null);
treeEl.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0);
treeEl.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0);
// Activate node on right-click
node.setActive();
@@ -108,10 +127,10 @@ const contextMenu = (function() {
protected_session.protectSubTree(node.data.note_id, false);
}
else if (ui.cmd === "copy") {
copy(node);
copy(noteTree.getSelectedNodes());
}
else if (ui.cmd === "cut") {
cut(node);
cut(noteTree.getSelectedNodes());
}
else if (ui.cmd === "pasteAfter") {
pasteAfter(node);
@@ -125,6 +144,9 @@ const contextMenu = (function() {
else if (ui.cmd === "collapse-sub-tree") {
noteTree.collapseTree(node);
}
else if (ui.cmd === "force-note-sync") {
forceNoteSync(node.data.note_id);
}
else {
messaging.logError("Unknown command: " + ui.cmd);
}

View File

@@ -9,7 +9,8 @@ const noteSource = (function() {
dialogEl.dialog({
modal: true,
width: 800
width: 800,
height: 500
});
const noteText = noteEditor.getCurrentNote().detail.note_text;

View File

@@ -46,14 +46,19 @@ const dragAndDropSetup = {
// This function MUST be defined to enable dropping of items on the tree.
// data.hitMode is 'before', 'after', or 'over'.
const nodeToMove = data.otherNode;
nodeToMove.setSelected(true);
const selectedNodes = noteTree.getSelectedNodes();
if (data.hitMode === "before") {
treeChanges.moveBeforeNode(data.otherNode, node);
treeChanges.moveBeforeNode(selectedNodes, node);
}
else if (data.hitMode === "after") {
treeChanges.moveAfterNode(data.otherNode, node);
treeChanges.moveAfterNode(selectedNodes, node);
}
else if (data.hitMode === "over") {
treeChanges.moveToNode(data.otherNode, node);
treeChanges.moveToNode(selectedNodes, node);
}
else {
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 remote = require('electron').remote;
const inPageSearch = searchInPage(remote.getCurrentWebContents(), { openDevToolsOfSearchWindow: true });
const inPageSearch = searchInPage(remote.getCurrentWebContents());
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', () => {
// 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
@@ -164,4 +200,6 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
messaging.logError(message);
return false;
};
};
$("#logout-button").toggle(!isElectron());

View File

@@ -128,6 +128,9 @@ const noteEditor = (function() {
setNoteBackgroundIfProtected(currentNote);
noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
// after loading new note make sure editor is scrolled to the top
noteDetailWrapperEl.scrollTop(0);
showAppIfHidden();
}

View File

@@ -4,6 +4,7 @@ const noteTree = (function() {
const treeEl = $("#tree");
const parentListEl = $("#parent-list");
const parentListListEl = $("#parent-list-list");
const noteDetailEl = $("#note-detail");
let startNotePath = null;
let notesTreeMap = {};
@@ -59,23 +60,6 @@ const noteTree = (function() {
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) {
assertArguments(noteTreeId);
@@ -185,13 +169,15 @@ const noteTree = (function() {
const noteTreeId = getNoteTreeId(parentNoteId, noteId);
const noteTree = notesTreeMap[noteTreeId];
const title = (noteTree.prefix ? (noteTree.prefix + " - ") : "") + noteIdToTitle[noteTree.note_id];
const node = {
note_id: noteTree.note_id,
parent_note_id: noteTree.parent_note_id,
note_tree_id: noteTree.note_tree_id,
is_protected: noteTree.is_protected,
prefix: noteTree.prefix,
title: (noteTree.prefix ? (noteTree.prefix + " - ") : "") + noteIdToTitle[noteTree.note_id],
title: escapeHtml(title),
extraClasses: getExtraClasses(noteTree),
refKey: noteTree.note_id,
expanded: noteTree.is_expanded
@@ -223,8 +209,6 @@ const noteTree = (function() {
let parentNoteId = 'root';
//console.log(now(), "Run path: ", runPath);
for (const childNoteId of runPath) {
const node = getNodesByNoteId(childNoteId).find(node => node.data.parent_note_id === parentNoteId);
@@ -237,6 +221,8 @@ const noteTree = (function() {
parentNoteId = childNoteId;
}
clearSelectedNodes();
}
/**
@@ -396,6 +382,22 @@ const noteTree = (function() {
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) {
assertArguments(noteTree);
@@ -403,28 +405,62 @@ const noteTree = (function() {
"del": node => {
treeChanges.deleteNode(node);
},
"shift+up": node => {
"ctrl+up": node => {
const beforeNode = node.getPrevSibling();
if (beforeNode !== null) {
treeChanges.moveBeforeNode(node, beforeNode);
treeChanges.moveBeforeNode([node], beforeNode);
}
return false;
},
"shift+down": node => {
"ctrl+down": node => {
let afterNode = node.getNextSibling();
if (afterNode !== null) {
treeChanges.moveAfterNode(node, afterNode);
treeChanges.moveAfterNode([node], afterNode);
}
return false;
},
"shift+left": node => {
"ctrl+left": node => {
treeChanges.moveNodeUpInHierarchy(node);
return false;
},
"shift+right": node => {
"ctrl+right": node => {
let toNode = node.getPrevSibling();
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 => {
editTreePrefix.showDialog(node);
@@ -432,26 +468,54 @@ const noteTree = (function() {
"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
// after opening context menu, standard shortcuts don't work, but they are detected here
// so we essentially takeover the standard handling with our implementation.
"left": node => {
node.navigate($.ui.keyCode.LEFT, true);
node.navigate($.ui.keyCode.LEFT, true).then(() => clearSelectedNodes());
return false;
},
"right": node => {
node.navigate($.ui.keyCode.RIGHT, true);
node.navigate($.ui.keyCode.RIGHT, true).then(() => clearSelectedNodes());
return false;
},
"up": node => {
node.navigate($.ui.keyCode.UP, true);
node.navigate($.ui.keyCode.UP, true).then(() => clearSelectedNodes());
return false;
},
"down": node => {
node.navigate($.ui.keyCode.DOWN, true);
node.navigate($.ui.keyCode.DOWN, true).then(() => clearSelectedNodes());
return false;
}
@@ -463,6 +527,22 @@ const noteTree = (function() {
extensions: ["hotkeys", "filter", "dnd", "clones"],
source: noteTree,
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) => {
const node = data.node.data;
@@ -513,57 +593,6 @@ const noteTree = (function() {
mode: "hide" // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
},
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){
const node = data.node.data;
@@ -772,7 +801,11 @@ const noteTree = (function() {
$(window).bind('hashchange', function() {
const notePath = getNotePathFromAddress();
activateNode(notePath);
if (getCurrentNotePath() !== notePath) {
console.log("Switching to " + notePath + " because of hash change");
activateNode(notePath);
}
});
if (isElectron()) {
@@ -807,6 +840,7 @@ const noteTree = (function() {
setPrefix,
getNotePathTitle,
removeParentChildRelation,
setParentChildRelation
setParentChildRelation,
getSelectedNodes
};
})();

View File

@@ -13,4 +13,10 @@ async function syncNow() {
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";
const treeChanges = (function() {
async function moveBeforeNode(node, beforeNode) {
await server.put('notes/' + node.data.note_tree_id + '/move-before/' + beforeNode.data.note_tree_id);
async function moveBeforeNode(nodesToMove, beforeNode) {
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) {
await server.put('notes/' + node.data.note_tree_id + '/move-after/' + afterNode.data.note_tree_id);
async function moveAfterNode(nodesToMove, afterNode) {
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!
@@ -25,17 +39,30 @@ const treeChanges = (function() {
await noteTree.reload();
}
async function moveToNode(node, toNode) {
await server.put('notes/' + node.data.note_tree_id + '/move-to/' + toNode.data.note_id);
async function moveToNode(nodesToMove, toNode) {
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 => {
node.moveTo(toNode);
if (!resp.success) {
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;
toNode.renderTitle();
});
node.moveTo(toNode);
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) {
@@ -92,7 +119,12 @@ const treeChanges = (function() {
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) {
node.getParent().folder = false;

View File

@@ -37,7 +37,7 @@ const treeUtils = (function() {
const title = (prefix ? (prefix + " - ") : "") + noteTitle;
node.setTitle(title);
node.setTitle(escapeHtml(title));
}
return {

View File

@@ -93,4 +93,8 @@ function isTopLevelNode(node) {
function isRootNode(node) {
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;
}
/* 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 {
max-height: 300px;
overflow-y: auto;
@@ -169,7 +175,6 @@ div.ui-tooltip {
/* Allow to use <kbd> elements inside the title to define shortcut hints. */
.ui-menu kbd, button kbd {
float: right;
color: black;
border: none;
background-color: transparent;
@@ -178,6 +183,7 @@ div.ui-tooltip {
.ui-menu kbd {
margin-left: 30px;
float: right;
}
#note-id-display {
@@ -185,6 +191,12 @@ div.ui-tooltip {
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{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}

View File

@@ -12,6 +12,15 @@ router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, async (req,
const parentNoteId = req.params.parentNoteId;
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 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);
});
res.send({});
res.send({ success: true });
});
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 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]);
if (!await checkTreeCycle(beforeNote.parent_note_id, noteToMove.note_id)) {
return res.send({
success: false,
message: 'Moving note here would create cycle.'
});
}
if (beforeNote) {
await sql.doInTransaction(async () => {
// 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);
});
res.send({});
res.send({ success: true });
}
else {
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 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]);
if (!await checkTreeCycle(afterNote.parent_note_id, noteToMove.note_id)) {
return res.send({
success: false,
message: 'Moving note here would create cycle.'
});
}
if (afterNote) {
await sql.doInTransaction(async () => {
// 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);
});
res.send({});
res.send({ success: true });
}
else {
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({
success: false,
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]);
});
res.send({
success: true
});
res.send({ success: true });
});
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.");
}
if (!await checkCycle(afterNote.parent_note_id, noteId)) {
if (!await checkTreeCycle(afterNote.parent_note_id, noteId)) {
return res.send({
success: false,
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);
});
res.send({
success: true
});
res.send({ success: true });
});
async function checkCycle(parentNoteId, childNoteId) {
if (parentNoteId === 'root') {
async function loadSubTreeNoteIds(parentNoteId, subTreeNoteIds) {
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;
}
if (parentNoteId === childNoteId) {
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;
return await checkTreeCycleInner(parentNoteId);
}
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({});
});
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) => {
const lastSyncId = parseInt(req.query.lastSyncId);

View File

@@ -3,12 +3,9 @@
const migration = require('./migration');
const sql = require('./sql');
const utils = require('./utils');
const options = require('./options');
async function checkAuth(req, res, next) {
const username = await options.getOption('username');
if (!username) {
if (!await sql.isUserInitialized()) {
res.redirect("setup");
}
else if (!req.session.loggedIn && !utils.isElectron()) {
@@ -53,9 +50,7 @@ async function checkApiAuthForMigrationPage(req, res, next) {
}
async function checkAppNotInitialized(req, res, next) {
const username = await options.getOption('username');
if (username) {
if (await sql.isUserInitialized()) {
res.status(400).send("App already initialized.");
}
else {

View File

@@ -1 +1 @@
module.exports = { build_date:"2017-12-26T19:57:44-05:00", build_revision: "baab7454626b154b43144c1a07e1962ab083bde2" };
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 dataDir = require('./data_dir');
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';

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) {
await runCheck(`
SELECT
@@ -43,6 +83,8 @@ async function runSyncRowChecks(table, key, errorList) {
async function runChecks() {
const errorList = [];
const startTime = new Date();
await runCheck(`
SELECT
note_id
@@ -124,11 +166,21 @@ async function runChecks() {
await runSyncRowChecks("notes_tree", "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) {
log.info(`Consistency checks failed (took ${elapsedTimeMs}ms) with these errors: ` + JSON.stringify(errorList));
messaging.sendMessageToAllClients({type: 'consistency-checks-failed'});
}
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 utils = require('./utils');
const options = require('./options');
const log = require('./log');
function getHash(rows) {
let hash = '';
@@ -13,9 +14,11 @@ function getHash(rows) {
}
async function getHashes() {
const startTime = new Date();
const optionsQuestionMarks = Array(options.SYNCED_OPTIONS.length).fill('?').join(',');
return {
const hashes = {
notes: getHash(await sql.getAll(`SELECT
note_id,
note_title,
@@ -62,6 +65,12 @@ async function getHashes() {
WHERE opt_name IN (${optionsQuestionMarks})
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 = {

View File

@@ -1,6 +1,7 @@
"use strict";
const crypto = require('crypto');
const log = require('./log');
function arraysIdentical(a, b) {
let i = a.length;
@@ -72,7 +73,15 @@ function decrypt(key, iv, cipherText) {
function decryptString(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) {

View File

@@ -54,6 +54,8 @@ async function sendMessage(client, message) {
async function sendMessageToAllClients(message) {
const jsonStr = JSON.stringify(message);
log.info("Sending message to all clients: " + jsonStr);
webSocketServer.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(jsonStr);

View File

@@ -3,14 +3,7 @@ const sql = require('./sql');
const options = require('./options');
const fs = require('fs-extra');
const log = require('./log');
const path = require('path');
const MIGRATIONS_DIR = path.resolve(__dirname, "..", "migrations");
if (!fs.existsSync(MIGRATIONS_DIR)) {
log.error("Could not find migration directory: " + MIGRATIONS_DIR);
process.exit(1);
}
const resource_dir = require('./resource_dir');
async function migrate() {
const migrations = [];
@@ -20,7 +13,7 @@ async function migrate() {
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)/);
if (match) {
@@ -53,7 +46,7 @@ async function migrate() {
await sql.doInTransaction(async () => {
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);
@@ -62,7 +55,7 @@ async function migrate() {
else if (mig.type === 'js') {
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);
}
else {

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 fs = require('fs');
const sqlite = require('sqlite');
const utils = require('./utils');
const app_info = require('./app_info');
const resource_dir = require('./resource_dir');
async function createConnection() {
return await sqlite.open(dataDir.DOCUMENT_PATH, {Promise});
@@ -28,9 +28,9 @@ const dbReady = new Promise((resolve, reject) => {
if (tableResults.length !== 1) {
log.info("Connected to db, but schema doesn't exist. Initializing schema ...");
const schema = fs.readFileSync('db/schema.sql', 'UTF-8');
const notesSql = fs.readFileSync('db/main_notes.sql', 'UTF-8');
const notesTreeSql = fs.readFileSync('db/main_notes_tree.sql', 'UTF-8');
const schema = fs.readFileSync(resource_dir.DB_INIT_DIR + '/schema.sql', 'UTF-8');
const notesSql = fs.readFileSync(resource_dir.DB_INIT_DIR + '/main_notes.sql', 'UTF-8');
const notesTreeSql = fs.readFileSync(resource_dir.DB_INIT_DIR + '/main_notes_tree.sql', 'UTF-8');
await doInTransaction(async () => {
await executeScript(schema);
@@ -49,9 +49,7 @@ const dbReady = new Promise((resolve, reject) => {
// the database
}
else {
const username = await getFirstValue("SELECT opt_value FROM options WHERE opt_name = 'username'");
if (!username) {
if (!await isUserInitialized()) {
log.info("Login/password not initialized. DB not ready.");
return;
@@ -235,8 +233,15 @@ async function isDbUpToDate() {
return upToDate;
}
async function isUserInitialized() {
const username = await getFirstValue("SELECT opt_value FROM options WHERE opt_name = 'username'");
return !!username;
}
module.exports = {
dbReady,
isUserInitialized,
insert,
replace,
getFirstValue,

View File

@@ -24,8 +24,8 @@ async function addOptionsSync(optName, sourceId) {
await addEntitySync("options", optName, sourceId);
}
async function addRecentNoteSync(notePath, sourceId) {
await addEntitySync("recent_notes", notePath, sourceId);
async function addRecentNoteSync(noteTreeId, sourceId) {
await addEntitySync("recent_notes", noteTreeId, 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]);
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 sync_table.addNoteHistorySync(entity.note_history_id, sourceId);

View File

@@ -29,7 +29,7 @@
<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">
</form>
</div>
@@ -172,11 +172,11 @@
<div id="protected-session-password-dialog" title="Protected session" style="display: none;">
<form id="protected-session-password-form">
<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" class="form-control" type="password">
</div>
<button class="btn btn-sm">Enter protected session <kbd>enter</kbd></button>
<button class="btn btn-sm">Start protected session <kbd>enter</kbd></button>
</form>
</div>
@@ -327,7 +327,7 @@
</div>
<div id="note-source-dialog" title="Note source" style="display: none; padding: 20px;">
<pre id="note-source"></pre>
<textarea id="note-source" readonly="readonly"></textarea>
</div>
<div id="tooltip" style="display: none;"></div>

View File

@@ -48,6 +48,5 @@
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
<link href="libraries/bootstrap/css/bootstrap.css" rel="stylesheet">
<script src="libraries/bootstrap/js/bootstrap.js"></script>
</body>
</html>