mirror of
https://github.com/zadam/trilium.git
synced 2025-10-27 16:26:31 +01:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
361d8a4216 | ||
|
|
ae6e222c50 | ||
|
|
37995f1ce5 | ||
|
|
ad7fa5e096 | ||
|
|
3585982758 | ||
|
|
c776f298f2 | ||
|
|
f07c427da1 | ||
|
|
e560072f8b | ||
|
|
3f976a3821 | ||
|
|
274bb32696 | ||
|
|
99b163a042 | ||
|
|
fdcc833f6d | ||
|
|
a8e45019e4 | ||
|
|
7f7028873c | ||
|
|
2d2d76a715 | ||
|
|
69cbfaae17 | ||
|
|
aebce8f12b | ||
|
|
045ca1f0bf | ||
|
|
bf2db6eac7 | ||
|
|
cf84114f91 | ||
|
|
6426157bb3 | ||
|
|
332fc16852 | ||
|
|
da2cd57428 | ||
|
|
de9bab1181 | ||
|
|
136375cf11 | ||
|
|
eabc7f80b7 | ||
|
|
6405d6e066 | ||
|
|
f6d481a9e2 | ||
|
|
695f0e5879 | ||
|
|
ae337e4500 | ||
|
|
19ffa14f10 | ||
|
|
bf3f26fde8 |
10
bin/build.sh
10
bin/build.sh
@@ -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/
|
||||
|
||||
@@ -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
|
||||
@@ -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!"
|
||||
1
index.js
1
index.js
@@ -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')
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ const noteSource = (function() {
|
||||
|
||||
dialogEl.dialog({
|
||||
modal: true,
|
||||
width: 800
|
||||
width: 800,
|
||||
height: 500
|
||||
});
|
||||
|
||||
const noteText = noteEditor.getCurrentNote().detail.note_text;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
})();
|
||||
@@ -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.");
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -37,7 +37,7 @@ const treeUtils = (function() {
|
||||
|
||||
const title = (prefix ? (prefix + " - ") : "") + noteTitle;
|
||||
|
||||
node.setTitle(title);
|
||||
node.setTitle(escapeHtml(title));
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -93,4 +93,8 @@ function isTopLevelNode(node) {
|
||||
|
||||
function isRootNode(node) {
|
||||
return node.key === "root_1";
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return $('<div/>').text(str).html();
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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)`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
25
services/resource_dir.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const log = require('./log');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const RESOURCE_DIR = path.resolve(__dirname, "..");
|
||||
|
||||
const MIGRATIONS_DIR = path.resolve(RESOURCE_DIR, "migrations");
|
||||
|
||||
if (!fs.existsSync(MIGRATIONS_DIR)) {
|
||||
log.error("Could not find migration directory: " + MIGRATIONS_DIR);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const DB_INIT_DIR = path.resolve(RESOURCE_DIR, "db");
|
||||
|
||||
if (!fs.existsSync(DB_INIT_DIR)) {
|
||||
log.error("Could not find DB initialization directory: " + DB_INIT_DIR);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RESOURCE_DIR,
|
||||
MIGRATIONS_DIR,
|
||||
DB_INIT_DIR
|
||||
};
|
||||
@@ -4,8 +4,8 @@ const log = require('./log');
|
||||
const dataDir = require('./data_dir');
|
||||
const 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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user