mirror of
https://github.com/zadam/trilium.git
synced 2025-10-27 16:26:31 +01:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d9b0db316 | ||
|
|
96a44a9a0c | ||
|
|
b545100cad | ||
|
|
e32289720c | ||
|
|
550bb77ca9 | ||
|
|
664a87cdd5 | ||
|
|
53ee1fa5ed | ||
|
|
361d8a4216 | ||
|
|
ae6e222c50 | ||
|
|
37995f1ce5 | ||
|
|
ad7fa5e096 | ||
|
|
3585982758 | ||
|
|
c776f298f2 | ||
|
|
f07c427da1 | ||
|
|
e560072f8b | ||
|
|
3f976a3821 | ||
|
|
274bb32696 | ||
|
|
99b163a042 | ||
|
|
fdcc833f6d | ||
|
|
a8e45019e4 | ||
|
|
7f7028873c | ||
|
|
2d2d76a715 | ||
|
|
69cbfaae17 | ||
|
|
aebce8f12b | ||
|
|
045ca1f0bf | ||
|
|
bf2db6eac7 | ||
|
|
cf84114f91 | ||
|
|
6426157bb3 | ||
|
|
332fc16852 | ||
|
|
da2cd57428 | ||
|
|
de9bab1181 | ||
|
|
136375cf11 | ||
|
|
eabc7f80b7 | ||
|
|
6405d6e066 | ||
|
|
f6d481a9e2 | ||
|
|
695f0e5879 | ||
|
|
ae337e4500 | ||
|
|
19ffa14f10 | ||
|
|
bf3f26fde8 |
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/
|
cp -r ../trilium-node-binaries/scrypt/* node_modules/scrypt/bin/
|
||||||
|
|
||||||
./node_modules/.bin/electron-rebuild
|
./node_modules/.bin/electron-rebuild --arch=ia32
|
||||||
|
|
||||||
./node_modules/.bin/electron-packager . --out=dist --platform=linux,win32 --overwrite
|
./node_modules/.bin/electron-packager . --out=dist --platform=linux --arch=ia32 --overwrite
|
||||||
|
|
||||||
|
./node_modules/.bin/electron-rebuild --arch=x64
|
||||||
|
|
||||||
|
./node_modules/.bin/electron-packager . --out=dist --platform=linux --arch=x64 --overwrite
|
||||||
|
|
||||||
|
./node_modules/.bin/electron-packager . --out=dist --platform=win32 --arch=x64 --overwrite
|
||||||
|
|
||||||
# can't copy this before the packaging because the same file name is used for both linux and windows build
|
# can't copy this before the packaging because the same file name is used for both linux and windows build
|
||||||
cp ../trilium-node-binaries/scrypt.node ./dist/trilium-win32-x64/resources/app/node_modules/scrypt/build/Release/
|
cp ../trilium-node-binaries/scrypt.node ./dist/trilium-win32-x64/resources/app/node_modules/scrypt/build/Release/
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ VERSION=`jq -r ".version" package.json`
|
|||||||
|
|
||||||
cd dist
|
cd dist
|
||||||
|
|
||||||
echo "Packaging windows electron distribution..."
|
echo "Packaging linux x64 electron distribution..."
|
||||||
7z a trilium-windows-${VERSION}.7z trilium-win32-x64
|
7z a trilium-linux-x64-${VERSION}.7z trilium-linux-x64
|
||||||
|
|
||||||
echo "Packaging linux electron distribution..."
|
echo "Packaging linux ia32 electron distribution..."
|
||||||
7z a trilium-linux-${VERSION}.7z trilium-linux-x64
|
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
|
bin/package.sh
|
||||||
|
|
||||||
LINUX_BUILD=trilium-linux-$VERSION.7z
|
LINUX_X64_BUILD=trilium-linux-x64-$VERSION.7z
|
||||||
WINDOWS_BUILD=trilium-windows-$VERSION.7z
|
LINUX_IA32_BUILD=trilium-linux-ia32-$VERSION.7z
|
||||||
|
WINDOWS_X64_BUILD=trilium-windows-x64-$VERSION.7z
|
||||||
|
|
||||||
echo "Creating release in GitHub"
|
echo "Creating release in GitHub"
|
||||||
|
|
||||||
@@ -53,18 +54,25 @@ github-release release \
|
|||||||
--tag $TAG \
|
--tag $TAG \
|
||||||
--name "$TAG release"
|
--name "$TAG release"
|
||||||
|
|
||||||
echo "Uploading linux build"
|
echo "Uploading linux x64 build"
|
||||||
|
|
||||||
github-release upload \
|
github-release upload \
|
||||||
--tag $TAG \
|
--tag $TAG \
|
||||||
--name "$LINUX_BUILD" \
|
--name "$LINUX_X64_BUILD" \
|
||||||
--file "dist/$LINUX_BUILD"
|
--file "dist/$LINUX_X64_BUILD"
|
||||||
|
|
||||||
echo "Uploading windows build"
|
echo "Uploading linux ia32 build"
|
||||||
|
|
||||||
github-release upload \
|
github-release upload \
|
||||||
--tag $TAG \
|
--tag $TAG \
|
||||||
--name "$WINDOWS_BUILD" \
|
--name "$LINUX_IA32_BUILD" \
|
||||||
--file "dist/$WINDOWS_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!"
|
echo "Release finished!"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('root', 'root', 'root', 0, 0, '2017-12-22T11:41:07.000Z', '2017-12-22T11:41:07.000Z');
|
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('root', 'root', 'root', 0, 0, '2017-12-22T11:41:07.000Z', '2017-12-22T11:41:07.000Z');
|
||||||
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('1Heh2acXfPNt', 'Trilium Demo', '<p>Welcome to Trilium Notes!</p><p> </p><p>This is initial document provided by default Trilium to showcase some of its features and also give you some ideas how you might structure your notes. You can play with it, modify note content and tree structure as you wish.</p><p> </p><p>If you need any help, visit Trilium wesite: <a href="https://github.com/zadam/trilium">https://github.com/zadam/trilium</a></p><p> </p><p>Once you''re finished with experimenting and want to cleanup these pages, you can simply delete them all.</p>', 0, 0, '2017-12-23T00:46:39.304Z', '2017-12-23T04:08:45.445Z');
|
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('1Heh2acXfPNt', 'Trilium Demo', '<p><strong>Welcome to Trilium Notes!</strong></p><p> </p><p>This is initial document provided by default Trilium to showcase some of its features and also give you some ideas how you might structure your notes. You can play with it, modify note content and tree structure as you wish.</p><p> </p><p>If you need any help, visit Trilium wesite: <a href="https://github.com/zadam/trilium">https://github.com/zadam/trilium</a></p><h3>Cleanup</h3><p>Once you''re finished with experimenting and want to cleanup these pages, you can simply delete them all.</p><h3>Formatting</h3><p>Trilium supports classic formatting like <i>italic</i>, <strong>bold</strong>, <i><strong>bold and italic</strong></i>. Of course you can add links like this one pointing to <a href="http://www.google.com">google.com</a></p><h4>Lists</h4><p><strong>Ordered:</strong></p><ol><li>First Item</li><li>Second item<ol><li>First sub-item</li><li>Second sub-item</li></ol></li></ol><p> </p><p><strong>Unordered:</strong></p><ul><li>Item</li><li>Another item<ul><li>Sub-item<ul><li>Sub-sub-item</li></ul></li></ul></li></ul><h4>Block quotes</h4><blockquote><p>Whereof one cannot speak, thereof one must be silent”</p><p>– Ludwig Wittgenstein</p></blockquote><p> </p>', 0, 0, '2017-12-23T00:46:39.304Z', '2017-12-23T04:08:45.445Z');
|
||||||
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('3RkyK9LI18dO', 'Journal', '<p>Expand note on the left pane to see content.</p>', 0, 0, '2017-12-23T01:20:04.181Z', '2017-12-23T18:07:55.377Z');
|
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('3RkyK9LI18dO', 'Journal', '<p>Expand note on the left pane to see content.</p>', 0, 0, '2017-12-23T01:20:04.181Z', '2017-12-23T18:07:55.377Z');
|
||||||
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('L1Ox40M1aEyy', '2016', '<p>No content.</p><p> </p><p> </p><p> </p><p> </p>', 0, 0, '2017-12-23T01:20:45.365Z', '2017-12-23T16:40:43.129Z');
|
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('L1Ox40M1aEyy', '2016', '<p>No content.</p><p> </p><p> </p><p> </p><p> </p>', 0, 0, '2017-12-23T01:20:45.365Z', '2017-12-23T16:40:43.129Z');
|
||||||
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('HJusZTbBU494', '2017', '<p>No content.</p>', 0, 0, '2017-12-23T01:20:50.709Z', '2017-12-23T16:41:03.119Z');
|
INSERT INTO notes (note_id, note_title, note_text, is_protected, is_deleted, date_created, date_modified) VALUES ('HJusZTbBU494', '2017', '<p>No content.</p>', 0, 0, '2017-12-23T01:20:50.709Z', '2017-12-23T16:41:03.119Z');
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ CREATE INDEX `IDX_notes_is_deleted` ON `notes` (
|
|||||||
CREATE INDEX `IDX_notes_tree_note_tree_id` ON `notes_tree` (
|
CREATE INDEX `IDX_notes_tree_note_tree_id` ON `notes_tree` (
|
||||||
`note_tree_id`
|
`note_tree_id`
|
||||||
);
|
);
|
||||||
CREATE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
|
CREATE UNIQUE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
|
||||||
`note_id`,
|
`note_id`,
|
||||||
`parent_note_id`
|
`parent_note_id`
|
||||||
);
|
);
|
||||||
|
|||||||
1
index.js
1
index.js
@@ -21,6 +21,7 @@ function createMainWindow() {
|
|||||||
const win = new electron.BrowserWindow({
|
const win = new electron.BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 900,
|
height: 900,
|
||||||
|
title: 'Trilium Notes',
|
||||||
icon: path.join(__dirname, 'public/images/app-icons/png/256x256.png')
|
icon: path.join(__dirname, 'public/images/app-icons/png/256x256.png')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
6
migrations/0061__change_index_to_unique.sql
Normal file
6
migrations/0061__change_index_to_unique.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
DROP INDEX IDX_notes_tree_note_id_parent_note_id;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
|
||||||
|
`note_id`,
|
||||||
|
`parent_note_id`
|
||||||
|
);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "trilium",
|
"name": "trilium",
|
||||||
"description": "Trilium Notes",
|
"description": "Trilium Notes",
|
||||||
"version": "0.0.11",
|
"version": "0.2.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./bin/www",
|
"start": "node ./bin/www",
|
||||||
"test-electron": "xo",
|
"test-electron": "xo",
|
||||||
|
|||||||
@@ -3,54 +3,72 @@
|
|||||||
const contextMenu = (function() {
|
const contextMenu = (function() {
|
||||||
const treeEl = $("#tree");
|
const treeEl = $("#tree");
|
||||||
|
|
||||||
let clipboardId = null;
|
let clipboardIds = [];
|
||||||
let clipboardMode = null;
|
let clipboardMode = null;
|
||||||
|
|
||||||
function pasteAfter(node) {
|
function pasteAfter(node) {
|
||||||
if (clipboardMode === 'cut') {
|
if (clipboardMode === 'cut') {
|
||||||
const subjectNode = treeUtils.getNodeByKey(clipboardId);
|
for (const nodeKey of clipboardIds) {
|
||||||
|
const subjectNode = treeUtils.getNodeByKey(nodeKey);
|
||||||
|
|
||||||
treeChanges.moveAfterNode(subjectNode, node);
|
treeChanges.moveAfterNode([subjectNode], node);
|
||||||
|
}
|
||||||
|
|
||||||
|
clipboardIds = [];
|
||||||
|
clipboardMode = null;
|
||||||
}
|
}
|
||||||
else if (clipboardMode === 'copy') {
|
else if (clipboardMode === 'copy') {
|
||||||
treeChanges.cloneNoteAfter(clipboardId, node.data.note_tree_id);
|
for (const noteId of clipboardIds) {
|
||||||
|
treeChanges.cloneNoteAfter(noteId, node.data.note_tree_id);
|
||||||
}
|
}
|
||||||
else if (clipboardId === null) {
|
|
||||||
|
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
|
||||||
|
}
|
||||||
|
else if (clipboardIds.length === 0) {
|
||||||
// just do nothing
|
// just do nothing
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throwError("Unrecognized clipboard mode=" + clipboardMode);
|
throwError("Unrecognized clipboard mode=" + clipboardMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
clipboardId = null;
|
|
||||||
clipboardMode = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pasteInto(node) {
|
function pasteInto(node) {
|
||||||
if (clipboardMode === 'cut') {
|
if (clipboardMode === 'cut') {
|
||||||
const subjectNode = treeUtils.getNodeByKey(clipboardId);
|
for (const nodeKey of clipboardIds) {
|
||||||
|
const subjectNode = treeUtils.getNodeByKey(nodeKey);
|
||||||
|
|
||||||
treeChanges.moveToNode(subjectNode, node);
|
treeChanges.moveToNode([subjectNode], node);
|
||||||
|
}
|
||||||
|
|
||||||
|
clipboardIds = [];
|
||||||
|
clipboardMode = null;
|
||||||
}
|
}
|
||||||
else if (clipboardMode === 'copy') {
|
else if (clipboardMode === 'copy') {
|
||||||
treeChanges.cloneNoteTo(clipboardId, node.data.note_id);
|
for (const noteId of clipboardIds) {
|
||||||
|
treeChanges.cloneNoteTo(noteId, node.data.note_id);
|
||||||
|
}
|
||||||
|
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
|
||||||
|
}
|
||||||
|
else if (clipboardIds.length === 0) {
|
||||||
|
// just do nothing
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throwError("Unrecognized clipboard mode=" + mode);
|
throwError("Unrecognized clipboard mode=" + mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
clipboardId = null;
|
|
||||||
clipboardMode = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function copy(node) {
|
function copy(nodes) {
|
||||||
clipboardId = node.data.note_id;
|
clipboardIds = nodes.map(node => node.data.note_id);
|
||||||
clipboardMode = 'copy';
|
clipboardMode = 'copy';
|
||||||
|
|
||||||
|
showMessage("Note(s) have been copied into clipboard.");
|
||||||
}
|
}
|
||||||
|
|
||||||
function cut(node) {
|
function cut(nodes) {
|
||||||
clipboardId = node.key;
|
clipboardIds = nodes.map(node => node.key);
|
||||||
clipboardMode = 'cut';
|
clipboardMode = 'cut';
|
||||||
|
|
||||||
|
showMessage("Note(s) have been cut into clipboard.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextMenuSettings = {
|
const contextMenuSettings = {
|
||||||
@@ -71,13 +89,14 @@ const contextMenu = (function() {
|
|||||||
{title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
|
{title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
|
||||||
{title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
|
{title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
|
||||||
{title: "----"},
|
{title: "----"},
|
||||||
{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) => {
|
beforeOpen: (event, ui) => {
|
||||||
const node = $.ui.fancytree.getNode(ui.target);
|
const node = $.ui.fancytree.getNode(ui.target);
|
||||||
// Modify menu entries depending on node status
|
// Modify menu entries depending on node status
|
||||||
treeEl.contextmenu("enableEntry", "pasteAfter", clipboardId !== null);
|
treeEl.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0);
|
||||||
treeEl.contextmenu("enableEntry", "pasteInto", clipboardId !== null);
|
treeEl.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0);
|
||||||
|
|
||||||
// Activate node on right-click
|
// Activate node on right-click
|
||||||
node.setActive();
|
node.setActive();
|
||||||
@@ -108,10 +127,10 @@ const contextMenu = (function() {
|
|||||||
protected_session.protectSubTree(node.data.note_id, false);
|
protected_session.protectSubTree(node.data.note_id, false);
|
||||||
}
|
}
|
||||||
else if (ui.cmd === "copy") {
|
else if (ui.cmd === "copy") {
|
||||||
copy(node);
|
copy(noteTree.getSelectedNodes());
|
||||||
}
|
}
|
||||||
else if (ui.cmd === "cut") {
|
else if (ui.cmd === "cut") {
|
||||||
cut(node);
|
cut(noteTree.getSelectedNodes());
|
||||||
}
|
}
|
||||||
else if (ui.cmd === "pasteAfter") {
|
else if (ui.cmd === "pasteAfter") {
|
||||||
pasteAfter(node);
|
pasteAfter(node);
|
||||||
@@ -125,6 +144,9 @@ const contextMenu = (function() {
|
|||||||
else if (ui.cmd === "collapse-sub-tree") {
|
else if (ui.cmd === "collapse-sub-tree") {
|
||||||
noteTree.collapseTree(node);
|
noteTree.collapseTree(node);
|
||||||
}
|
}
|
||||||
|
else if (ui.cmd === "force-note-sync") {
|
||||||
|
forceNoteSync(node.data.note_id);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
messaging.logError("Unknown command: " + ui.cmd);
|
messaging.logError("Unknown command: " + ui.cmd);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ const noteSource = (function() {
|
|||||||
|
|
||||||
dialogEl.dialog({
|
dialogEl.dialog({
|
||||||
modal: true,
|
modal: true,
|
||||||
width: 800
|
width: 800,
|
||||||
|
height: 500
|
||||||
});
|
});
|
||||||
|
|
||||||
const noteText = noteEditor.getCurrentNote().detail.note_text;
|
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.
|
// This function MUST be defined to enable dropping of items on the tree.
|
||||||
// data.hitMode is 'before', 'after', or 'over'.
|
// data.hitMode is 'before', 'after', or 'over'.
|
||||||
|
|
||||||
|
const nodeToMove = data.otherNode;
|
||||||
|
nodeToMove.setSelected(true);
|
||||||
|
|
||||||
|
const selectedNodes = noteTree.getSelectedNodes();
|
||||||
|
|
||||||
if (data.hitMode === "before") {
|
if (data.hitMode === "before") {
|
||||||
treeChanges.moveBeforeNode(data.otherNode, node);
|
treeChanges.moveBeforeNode(selectedNodes, node);
|
||||||
}
|
}
|
||||||
else if (data.hitMode === "after") {
|
else if (data.hitMode === "after") {
|
||||||
treeChanges.moveAfterNode(data.otherNode, node);
|
treeChanges.moveAfterNode(selectedNodes, node);
|
||||||
}
|
}
|
||||||
else if (data.hitMode === "over") {
|
else if (data.hitMode === "over") {
|
||||||
treeChanges.moveToNode(data.otherNode, node);
|
treeChanges.moveToNode(selectedNodes, node);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new Exception("Unknown hitMode=" + data.hitMode);
|
throw new Exception("Unknown hitMode=" + data.hitMode);
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ jQuery.hotkeys.options.filterContentEditable = false;
|
|||||||
jQuery.hotkeys.options.filterTextInputs = false;
|
jQuery.hotkeys.options.filterTextInputs = false;
|
||||||
|
|
||||||
$(document).bind('keydown', 'alt+m', e => {
|
$(document).bind('keydown', 'alt+m', e => {
|
||||||
const toggle = $(".hide-toggle");
|
$(".hide-toggle").toggleClass("suppressed");
|
||||||
const hidden = toggle.css('visibility') === 'hidden';
|
|
||||||
|
|
||||||
toggle.css('visibility', hidden ? 'visible' : 'hidden');
|
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
@@ -49,7 +46,7 @@ $(document).bind('keydown', 'ctrl+f', () => {
|
|||||||
const searchInPage = require('electron-in-page-search').default;
|
const searchInPage = require('electron-in-page-search').default;
|
||||||
const remote = require('electron').remote;
|
const remote = require('electron').remote;
|
||||||
|
|
||||||
const inPageSearch = searchInPage(remote.getCurrentWebContents(), { openDevToolsOfSearchWindow: true });
|
const inPageSearch = searchInPage(remote.getCurrentWebContents());
|
||||||
|
|
||||||
inPageSearch.openSearchWindow();
|
inPageSearch.openSearchWindow();
|
||||||
|
|
||||||
@@ -57,6 +54,42 @@ $(document).bind('keydown', 'ctrl+f', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(document).bind('keydown', "ctrl+shift+left", () => {
|
||||||
|
const node = noteTree.getCurrentNode();
|
||||||
|
node.navigate($.ui.keyCode.LEFT, true);
|
||||||
|
|
||||||
|
$("#note-detail").focus();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).bind('keydown', "ctrl+shift+right", () => {
|
||||||
|
const node = noteTree.getCurrentNode();
|
||||||
|
node.navigate($.ui.keyCode.RIGHT, true);
|
||||||
|
|
||||||
|
$("#note-detail").focus();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).bind('keydown', "ctrl+shift+up", () => {
|
||||||
|
const node = noteTree.getCurrentNode();
|
||||||
|
node.navigate($.ui.keyCode.UP, true);
|
||||||
|
|
||||||
|
$("#note-detail").focus();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).bind('keydown', "ctrl+shift+down", () => {
|
||||||
|
const node = noteTree.getCurrentNode();
|
||||||
|
node.navigate($.ui.keyCode.DOWN, true);
|
||||||
|
|
||||||
|
$("#note-detail").focus();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
$(window).on('beforeunload', () => {
|
$(window).on('beforeunload', () => {
|
||||||
// this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved
|
// this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved
|
||||||
// this sends the request asynchronously and doesn't wait for result
|
// this sends the request asynchronously and doesn't wait for result
|
||||||
@@ -122,27 +155,6 @@ $(document).tooltip({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let appShown = false;
|
|
||||||
|
|
||||||
function showAppIfHidden() {
|
|
||||||
if (!appShown) {
|
|
||||||
appShown = true;
|
|
||||||
|
|
||||||
$("#container").show();
|
|
||||||
|
|
||||||
// Get a reference to the loader's div
|
|
||||||
const loaderDiv = document.getElementById("loader-wrapper");
|
|
||||||
// When the transition ends remove loader's div from display
|
|
||||||
// so that we can access the map with gestures or clicks
|
|
||||||
loaderDiv.addEventListener("transitionend", function(){
|
|
||||||
loaderDiv.style.display = "none";
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
// Kick off the CSS transition
|
|
||||||
loaderDiv.style.opacity = 0.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||||
const string = msg.toLowerCase();
|
const string = msg.toLowerCase();
|
||||||
|
|
||||||
@@ -165,3 +177,5 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$("#logout-button").toggle(!isElectron());
|
||||||
@@ -42,6 +42,9 @@ const link = (function() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const linkEl = $(e.target);
|
const linkEl = $(e.target);
|
||||||
|
let notePath = linkEl.attr("note-path");
|
||||||
|
|
||||||
|
if (!notePath) {
|
||||||
const address = linkEl.attr("note-path") ? linkEl.attr("note-path") : linkEl.attr('href');
|
const address = linkEl.attr("note-path") ? linkEl.attr("note-path") : linkEl.attr('href');
|
||||||
|
|
||||||
if (!address) {
|
if (!address) {
|
||||||
@@ -54,7 +57,8 @@ const link = (function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const notePath = getNotePathFromLink(address);
|
notePath = getNotePathFromLink(address);
|
||||||
|
}
|
||||||
|
|
||||||
noteTree.activateNode(notePath);
|
noteTree.activateNode(notePath);
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,8 @@ const noteEditor = (function() {
|
|||||||
setNoteBackgroundIfProtected(currentNote);
|
setNoteBackgroundIfProtected(currentNote);
|
||||||
noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
|
noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
|
||||||
|
|
||||||
showAppIfHidden();
|
// after loading new note make sure editor is scrolled to the top
|
||||||
|
noteDetailWrapperEl.scrollTop(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadNote(noteId) {
|
async function loadNote(noteId) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const noteTree = (function() {
|
|||||||
const treeEl = $("#tree");
|
const treeEl = $("#tree");
|
||||||
const parentListEl = $("#parent-list");
|
const parentListEl = $("#parent-list");
|
||||||
const parentListListEl = $("#parent-list-list");
|
const parentListListEl = $("#parent-list-list");
|
||||||
|
const noteDetailEl = $("#note-detail");
|
||||||
|
|
||||||
let startNotePath = null;
|
let startNotePath = null;
|
||||||
let notesTreeMap = {};
|
let notesTreeMap = {};
|
||||||
@@ -59,23 +60,6 @@ const noteTree = (function() {
|
|||||||
return treeUtils.getNotePath(node);
|
return treeUtils.getNotePath(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentNoteId() {
|
|
||||||
const node = getCurrentNode();
|
|
||||||
|
|
||||||
return node ? node.data.note_id : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentClones() {
|
|
||||||
const noteId = getCurrentNoteId();
|
|
||||||
|
|
||||||
if (noteId) {
|
|
||||||
return getNodesByNoteId(noteId);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNodesByNoteTreeId(noteTreeId) {
|
function getNodesByNoteTreeId(noteTreeId) {
|
||||||
assertArguments(noteTreeId);
|
assertArguments(noteTreeId);
|
||||||
|
|
||||||
@@ -185,13 +169,15 @@ const noteTree = (function() {
|
|||||||
const noteTreeId = getNoteTreeId(parentNoteId, noteId);
|
const noteTreeId = getNoteTreeId(parentNoteId, noteId);
|
||||||
const noteTree = notesTreeMap[noteTreeId];
|
const noteTree = notesTreeMap[noteTreeId];
|
||||||
|
|
||||||
|
const title = (noteTree.prefix ? (noteTree.prefix + " - ") : "") + noteIdToTitle[noteTree.note_id];
|
||||||
|
|
||||||
const node = {
|
const node = {
|
||||||
note_id: noteTree.note_id,
|
note_id: noteTree.note_id,
|
||||||
parent_note_id: noteTree.parent_note_id,
|
parent_note_id: noteTree.parent_note_id,
|
||||||
note_tree_id: noteTree.note_tree_id,
|
note_tree_id: noteTree.note_tree_id,
|
||||||
is_protected: noteTree.is_protected,
|
is_protected: noteTree.is_protected,
|
||||||
prefix: noteTree.prefix,
|
prefix: noteTree.prefix,
|
||||||
title: (noteTree.prefix ? (noteTree.prefix + " - ") : "") + noteIdToTitle[noteTree.note_id],
|
title: escapeHtml(title),
|
||||||
extraClasses: getExtraClasses(noteTree),
|
extraClasses: getExtraClasses(noteTree),
|
||||||
refKey: noteTree.note_id,
|
refKey: noteTree.note_id,
|
||||||
expanded: noteTree.is_expanded
|
expanded: noteTree.is_expanded
|
||||||
@@ -223,8 +209,6 @@ const noteTree = (function() {
|
|||||||
|
|
||||||
let parentNoteId = 'root';
|
let parentNoteId = 'root';
|
||||||
|
|
||||||
//console.log(now(), "Run path: ", runPath);
|
|
||||||
|
|
||||||
for (const childNoteId of runPath) {
|
for (const childNoteId of runPath) {
|
||||||
const node = getNodesByNoteId(childNoteId).find(node => node.data.parent_note_id === parentNoteId);
|
const node = getNodesByNoteId(childNoteId).find(node => node.data.parent_note_id === parentNoteId);
|
||||||
|
|
||||||
@@ -237,6 +221,8 @@ const noteTree = (function() {
|
|||||||
|
|
||||||
parentNoteId = childNoteId;
|
parentNoteId = childNoteId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearSelectedNodes();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -396,6 +382,22 @@ const noteTree = (function() {
|
|||||||
recentNotes.addRecentNote(currentNoteTreeId, currentNotePath);
|
recentNotes.addRecentNote(currentNoteTreeId, currentNotePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSelectedNodes() {
|
||||||
|
return getTree().getSelectedNodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelectedNodes() {
|
||||||
|
for (const selectedNode of getSelectedNodes()) {
|
||||||
|
selectedNode.setSelected(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentNode = getCurrentNode();
|
||||||
|
|
||||||
|
if (currentNode) {
|
||||||
|
currentNode.setSelected(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initFancyTree(noteTree) {
|
function initFancyTree(noteTree) {
|
||||||
assertArguments(noteTree);
|
assertArguments(noteTree);
|
||||||
|
|
||||||
@@ -403,28 +405,62 @@ const noteTree = (function() {
|
|||||||
"del": node => {
|
"del": node => {
|
||||||
treeChanges.deleteNode(node);
|
treeChanges.deleteNode(node);
|
||||||
},
|
},
|
||||||
"shift+up": node => {
|
"ctrl+up": node => {
|
||||||
const beforeNode = node.getPrevSibling();
|
const beforeNode = node.getPrevSibling();
|
||||||
|
|
||||||
if (beforeNode !== null) {
|
if (beforeNode !== null) {
|
||||||
treeChanges.moveBeforeNode(node, beforeNode);
|
treeChanges.moveBeforeNode([node], beforeNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
"shift+down": node => {
|
"ctrl+down": node => {
|
||||||
let afterNode = node.getNextSibling();
|
let afterNode = node.getNextSibling();
|
||||||
if (afterNode !== null) {
|
if (afterNode !== null) {
|
||||||
treeChanges.moveAfterNode(node, afterNode);
|
treeChanges.moveAfterNode([node], afterNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
"shift+left": node => {
|
"ctrl+left": node => {
|
||||||
treeChanges.moveNodeUpInHierarchy(node);
|
treeChanges.moveNodeUpInHierarchy(node);
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
"shift+right": node => {
|
"ctrl+right": node => {
|
||||||
let toNode = node.getPrevSibling();
|
let toNode = node.getPrevSibling();
|
||||||
|
|
||||||
if (toNode !== null) {
|
if (toNode !== null) {
|
||||||
treeChanges.moveToNode(node, toNode);
|
treeChanges.moveToNode([node], toNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
"shift+up": node => {
|
||||||
|
node.navigate($.ui.keyCode.UP, true).then(() => {
|
||||||
|
const currentNode = getCurrentNode();
|
||||||
|
|
||||||
|
if (currentNode.isSelected()) {
|
||||||
|
node.setSelected(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentNode.setSelected(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
"shift+down": node => {
|
||||||
|
node.navigate($.ui.keyCode.DOWN, true).then(() => {
|
||||||
|
const currentNode = getCurrentNode();
|
||||||
|
|
||||||
|
if (currentNode.isSelected()) {
|
||||||
|
node.setSelected(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentNode.setSelected(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
"f2": node => {
|
"f2": node => {
|
||||||
editTreePrefix.showDialog(node);
|
editTreePrefix.showDialog(node);
|
||||||
@@ -432,26 +468,54 @@ const noteTree = (function() {
|
|||||||
"alt+-": node => {
|
"alt+-": node => {
|
||||||
collapseTree(node);
|
collapseTree(node);
|
||||||
},
|
},
|
||||||
|
"ctrl+a": node => {
|
||||||
|
for (const child of node.getParent().getChildren()) {
|
||||||
|
child.setSelected(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
"ctrl+c": () => {
|
||||||
|
contextMenu.copy(getSelectedNodes());
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
"ctrl+x": () => {
|
||||||
|
contextMenu.cut(getSelectedNodes());
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
"ctrl+v": node => {
|
||||||
|
contextMenu.pasteInto(node);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
"ctrl+return": node => {
|
||||||
|
noteDetailEl.focus();
|
||||||
|
},
|
||||||
|
"return": node => {
|
||||||
|
noteDetailEl.focus();
|
||||||
|
},
|
||||||
// code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin
|
// code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin
|
||||||
// after opening context menu, standard shortcuts don't work, but they are detected here
|
// after opening context menu, standard shortcuts don't work, but they are detected here
|
||||||
// so we essentially takeover the standard handling with our implementation.
|
// so we essentially takeover the standard handling with our implementation.
|
||||||
"left": node => {
|
"left": node => {
|
||||||
node.navigate($.ui.keyCode.LEFT, true);
|
node.navigate($.ui.keyCode.LEFT, true).then(() => clearSelectedNodes());
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
"right": node => {
|
"right": node => {
|
||||||
node.navigate($.ui.keyCode.RIGHT, true);
|
node.navigate($.ui.keyCode.RIGHT, true).then(() => clearSelectedNodes());
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
"up": node => {
|
"up": node => {
|
||||||
node.navigate($.ui.keyCode.UP, true);
|
node.navigate($.ui.keyCode.UP, true).then(() => clearSelectedNodes());
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
"down": node => {
|
"down": node => {
|
||||||
node.navigate($.ui.keyCode.DOWN, true);
|
node.navigate($.ui.keyCode.DOWN, true).then(() => clearSelectedNodes());
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -463,6 +527,22 @@ const noteTree = (function() {
|
|||||||
extensions: ["hotkeys", "filter", "dnd", "clones"],
|
extensions: ["hotkeys", "filter", "dnd", "clones"],
|
||||||
source: noteTree,
|
source: noteTree,
|
||||||
scrollParent: $("#tree"),
|
scrollParent: $("#tree"),
|
||||||
|
click: (event, data) => {
|
||||||
|
const targetType = data.targetType;
|
||||||
|
const node = data.node;
|
||||||
|
|
||||||
|
if (targetType === 'title' || targetType === 'icon') {
|
||||||
|
node.setActive();
|
||||||
|
|
||||||
|
if (!event.ctrlKey) {
|
||||||
|
clearSelectedNodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
node.setSelected(true);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
activate: (event, data) => {
|
activate: (event, data) => {
|
||||||
const node = data.node.data;
|
const node = data.node.data;
|
||||||
|
|
||||||
@@ -493,9 +573,6 @@ const noteTree = (function() {
|
|||||||
// so waiting a second helps
|
// so waiting a second helps
|
||||||
setTimeout(scrollToCurrentNote, 1000);
|
setTimeout(scrollToCurrentNote, 1000);
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
showAppIfHidden();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
hotkeys: {
|
hotkeys: {
|
||||||
keydown: keybindings
|
keydown: keybindings
|
||||||
@@ -513,57 +590,6 @@ const noteTree = (function() {
|
|||||||
mode: "hide" // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
|
mode: "hide" // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
|
||||||
},
|
},
|
||||||
dnd: dragAndDropSetup,
|
dnd: dragAndDropSetup,
|
||||||
keydown: (event, data) => {
|
|
||||||
const node = data.node;
|
|
||||||
// Eat keyboard events, when a menu is open
|
|
||||||
if ($(".contextMenu:visible").length > 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
switch (event.which) {
|
|
||||||
// Open context menu on [Space] key (simulate right click)
|
|
||||||
case 32: // [Space]
|
|
||||||
$(node.span).trigger("mousedown", {
|
|
||||||
preventDefault: true,
|
|
||||||
button: 2
|
|
||||||
})
|
|
||||||
.trigger("mouseup", {
|
|
||||||
preventDefault: true,
|
|
||||||
pageX: node.span.offsetLeft,
|
|
||||||
pageY: node.span.offsetTop,
|
|
||||||
button: 2
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Handle Ctrl+C, +X and +V
|
|
||||||
case 67:
|
|
||||||
if (event.ctrlKey) { // Ctrl+C
|
|
||||||
contextMenu.copy(node);
|
|
||||||
|
|
||||||
showMessage("Note copied into clipboard.");
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 88:
|
|
||||||
if (event.ctrlKey) { // Ctrl+X
|
|
||||||
contextMenu.cut(node);
|
|
||||||
|
|
||||||
showMessage("Note cut into clipboard.");
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 86:
|
|
||||||
if (event.ctrlKey) { // Ctrl+V
|
|
||||||
contextMenu.pasteInto(node);
|
|
||||||
|
|
||||||
showMessage("Note pasted from clipboard into current note.");
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
lazyLoad: function(event, data){
|
lazyLoad: function(event, data){
|
||||||
const node = data.node.data;
|
const node = data.node.data;
|
||||||
|
|
||||||
@@ -772,7 +798,11 @@ const noteTree = (function() {
|
|||||||
$(window).bind('hashchange', function() {
|
$(window).bind('hashchange', function() {
|
||||||
const notePath = getNotePathFromAddress();
|
const notePath = getNotePathFromAddress();
|
||||||
|
|
||||||
|
if (getCurrentNotePath() !== notePath) {
|
||||||
|
console.log("Switching to " + notePath + " because of hash change");
|
||||||
|
|
||||||
activateNode(notePath);
|
activateNode(notePath);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
@@ -807,6 +837,7 @@ const noteTree = (function() {
|
|||||||
setPrefix,
|
setPrefix,
|
||||||
getNotePathTitle,
|
getNotePathTitle,
|
||||||
removeParentChildRelation,
|
removeParentChildRelation,
|
||||||
setParentChildRelation
|
setParentChildRelation,
|
||||||
|
getSelectedNodes
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
@@ -22,9 +22,6 @@ const protected_session = (function() {
|
|||||||
const dfd = $.Deferred();
|
const dfd = $.Deferred();
|
||||||
|
|
||||||
if (requireProtectedSession && !isProtectedSessionAvailable()) {
|
if (requireProtectedSession && !isProtectedSessionAvailable()) {
|
||||||
// if this is entry point then we need to show the app even before the note is loaded
|
|
||||||
showAppIfHidden();
|
|
||||||
|
|
||||||
protectedSessionDeferred = dfd;
|
protectedSessionDeferred = dfd;
|
||||||
|
|
||||||
dialogEl.dialog({
|
dialogEl.dialog({
|
||||||
|
|||||||
@@ -14,3 +14,9 @@ async function syncNow() {
|
|||||||
showError("Sync failed: " + result.message);
|
showError("Sync failed: " + result.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function forceNoteSync(noteId) {
|
||||||
|
const result = await server.post('sync/force-note-sync/' + noteId);
|
||||||
|
|
||||||
|
showMessage("Note added to sync queue.");
|
||||||
|
}
|
||||||
@@ -1,16 +1,30 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const treeChanges = (function() {
|
const treeChanges = (function() {
|
||||||
async function moveBeforeNode(node, beforeNode) {
|
async function moveBeforeNode(nodesToMove, beforeNode) {
|
||||||
await server.put('notes/' + node.data.note_tree_id + '/move-before/' + beforeNode.data.note_tree_id);
|
for (const nodeToMove of nodesToMove) {
|
||||||
|
const resp = await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-before/' + beforeNode.data.note_tree_id);
|
||||||
|
|
||||||
changeNode(node, node => node.moveTo(beforeNode, 'before'));
|
if (!resp.success) {
|
||||||
|
alert(resp.message);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function moveAfterNode(node, afterNode) {
|
changeNode(nodeToMove, node => node.moveTo(beforeNode, 'before'));
|
||||||
await server.put('notes/' + node.data.note_tree_id + '/move-after/' + afterNode.data.note_tree_id);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
changeNode(node, node => node.moveTo(afterNode, 'after'));
|
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);
|
||||||
|
|
||||||
|
if (!resp.success) {
|
||||||
|
alert(resp.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeNode(nodeToMove, node => node.moveTo(afterNode, 'after'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// beware that first arg is noteId and second is noteTreeId!
|
// beware that first arg is noteId and second is noteTreeId!
|
||||||
@@ -25,18 +39,31 @@ const treeChanges = (function() {
|
|||||||
await noteTree.reload();
|
await noteTree.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function moveToNode(node, toNode) {
|
async function moveToNode(nodesToMove, toNode) {
|
||||||
await server.put('notes/' + node.data.note_tree_id + '/move-to/' + toNode.data.note_id);
|
for (const nodeToMove of nodesToMove) {
|
||||||
|
const resp = await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-to/' + toNode.data.note_id);
|
||||||
|
|
||||||
changeNode(node, node => {
|
if (!resp.success) {
|
||||||
node.moveTo(toNode);
|
alert(resp.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.setExpanded(true);
|
||||||
|
|
||||||
|
node.moveTo(toNode);
|
||||||
|
|
||||||
toNode.folder = true;
|
toNode.folder = true;
|
||||||
toNode.renderTitle();
|
toNode.renderTitle();
|
||||||
|
|
||||||
|
// this expands the note in case it become the folder only after the move
|
||||||
|
toNode.setExpanded(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function cloneNoteTo(childNoteId, parentNoteId, prefix) {
|
async function cloneNoteTo(childNoteId, parentNoteId, prefix) {
|
||||||
const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, {
|
const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, {
|
||||||
@@ -92,7 +119,12 @@ const treeChanges = (function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await server.put('notes/' + node.data.note_tree_id + '/move-after/' + node.getParent().data.note_tree_id);
|
const resp = await server.put('notes/' + node.data.note_tree_id + '/move-after/' + node.getParent().data.note_tree_id);
|
||||||
|
|
||||||
|
if (!resp.success) {
|
||||||
|
alert(resp.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isTopLevelNode(node) && node.getParent().getChildren().length <= 1) {
|
if (!isTopLevelNode(node) && node.getParent().getChildren().length <= 1) {
|
||||||
node.getParent().folder = false;
|
node.getParent().folder = false;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const treeUtils = (function() {
|
|||||||
|
|
||||||
const title = (prefix ? (prefix + " - ") : "") + noteTitle;
|
const title = (prefix ? (prefix + " - ") : "") + noteTitle;
|
||||||
|
|
||||||
node.setTitle(title);
|
node.setTitle(escapeHtml(title));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -94,3 +94,7 @@ function isTopLevelNode(node) {
|
|||||||
function isRootNode(node) {
|
function isRootNode(node) {
|
||||||
return node.key === "root_1";
|
return node.key === "root_1";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return $('<div/>').text(str).html();
|
||||||
|
}
|
||||||
@@ -74,6 +74,12 @@ span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-tit
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* By default not focused active tree item is not easily visible, this makes it more visible */
|
||||||
|
span.fancytree-active:not(.fancytree-focused) .fancytree-title {
|
||||||
|
background-color: #ddd !important;
|
||||||
|
border-color: #555 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.ui-autocomplete {
|
.ui-autocomplete {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -169,7 +175,6 @@ div.ui-tooltip {
|
|||||||
|
|
||||||
/* Allow to use <kbd> elements inside the title to define shortcut hints. */
|
/* Allow to use <kbd> elements inside the title to define shortcut hints. */
|
||||||
.ui-menu kbd, button kbd {
|
.ui-menu kbd, button kbd {
|
||||||
float: right;
|
|
||||||
color: black;
|
color: black;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -178,6 +183,7 @@ div.ui-tooltip {
|
|||||||
|
|
||||||
.ui-menu kbd {
|
.ui-menu kbd {
|
||||||
margin-left: 30px;
|
margin-left: 30px;
|
||||||
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
#note-id-display {
|
#note-id-display {
|
||||||
@@ -185,9 +191,12 @@ div.ui-tooltip {
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#loader-wrapper{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1000;background-color:#fff;opacity:1;transition:opacity 2s ease}
|
#note-source {
|
||||||
#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}
|
height: 98%;
|
||||||
#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}
|
width: 100%;
|
||||||
#loader:after{content:"";position:absolute;top:15px;left:15px;right:15px;bottom:15px;border-radius:50%;border:3px solid transparent;border-top-color:#ddd;-webkit-animation:spin 1.5s linear infinite;animation:spin 1.5s linear infinite}
|
overflow: scroll;
|
||||||
@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg)}}
|
}
|
||||||
@keyframes spin{0%{-webkit-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg)}}
|
|
||||||
|
.suppressed {
|
||||||
|
filter: opacity(7%);
|
||||||
|
}
|
||||||
@@ -12,6 +12,24 @@ router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, async (req,
|
|||||||
const parentNoteId = req.params.parentNoteId;
|
const parentNoteId = req.params.parentNoteId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
|
const noteToMove = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
||||||
|
|
||||||
|
const existing = await getExistingNoteTree(parentNoteId, noteToMove.note_id);
|
||||||
|
|
||||||
|
if (existing && !existing.is_deleted && existing.note_tree_id !== noteTreeId) {
|
||||||
|
return res.send({
|
||||||
|
success: false,
|
||||||
|
message: 'This note already exists in target parent note.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await checkTreeCycle(parentNoteId, noteToMove.note_id)) {
|
||||||
|
return res.send({
|
||||||
|
success: false,
|
||||||
|
message: 'Moving note here would create cycle.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const maxNotePos = await sql.getFirstValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]);
|
const maxNotePos = await sql.getFirstValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]);
|
||||||
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 1;
|
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 1;
|
||||||
|
|
||||||
@@ -24,7 +42,7 @@ router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, async (req,
|
|||||||
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send({});
|
res.send({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
||||||
@@ -32,8 +50,25 @@ router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, asyn
|
|||||||
const beforeNoteTreeId = req.params.beforeNoteTreeId;
|
const beforeNoteTreeId = req.params.beforeNoteTreeId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
|
const noteToMove = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
||||||
const beforeNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [beforeNoteTreeId]);
|
const beforeNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [beforeNoteTreeId]);
|
||||||
|
|
||||||
|
const existing = await getExistingNoteTree(beforeNote.parent_note_id, noteToMove.note_id);
|
||||||
|
|
||||||
|
if (existing && !existing.is_deleted && existing.note_tree_id !== noteTreeId) {
|
||||||
|
return res.send({
|
||||||
|
success: false,
|
||||||
|
message: 'This note already exists in target parent note.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await checkTreeCycle(beforeNote.parent_note_id, noteToMove.note_id)) {
|
||||||
|
return res.send({
|
||||||
|
success: false,
|
||||||
|
message: 'Moving note here would create cycle.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (beforeNote) {
|
if (beforeNote) {
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
// we don't change date_modified so other changes are prioritized in case of conflict
|
// we don't change date_modified so other changes are prioritized in case of conflict
|
||||||
@@ -51,7 +86,7 @@ router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, asyn
|
|||||||
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send({});
|
res.send({ success: true });
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
res.status(500).send("Before note " + beforeNoteTreeId + " doesn't exist.");
|
res.status(500).send("Before note " + beforeNoteTreeId + " doesn't exist.");
|
||||||
@@ -63,8 +98,25 @@ router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, async
|
|||||||
const afterNoteTreeId = req.params.afterNoteTreeId;
|
const afterNoteTreeId = req.params.afterNoteTreeId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
|
const noteToMove = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
||||||
const afterNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [afterNoteTreeId]);
|
const afterNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [afterNoteTreeId]);
|
||||||
|
|
||||||
|
const existing = await getExistingNoteTree(afterNote.parent_note_id, noteToMove.note_id);
|
||||||
|
|
||||||
|
if (existing && !existing.is_deleted && existing.note_tree_id !== noteTreeId) {
|
||||||
|
return res.send({
|
||||||
|
success: false,
|
||||||
|
message: 'This note already exists in target parent note.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await checkTreeCycle(afterNote.parent_note_id, noteToMove.note_id)) {
|
||||||
|
return res.send({
|
||||||
|
success: false,
|
||||||
|
message: 'Moving note here would create cycle.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (afterNote) {
|
if (afterNote) {
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
// we don't change date_modified so other changes are prioritized in case of conflict
|
// we don't change date_modified so other changes are prioritized in case of conflict
|
||||||
@@ -80,7 +132,7 @@ router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, async
|
|||||||
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send({});
|
res.send({ success: true });
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
res.status(500).send("After note " + afterNoteTreeId + " doesn't exist.");
|
res.status(500).send("After note " + afterNoteTreeId + " doesn't exist.");
|
||||||
@@ -93,7 +145,7 @@ router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, async (req
|
|||||||
const prefix = req.body.prefix;
|
const prefix = req.body.prefix;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
const existing = await sql.getFirst('SELECT * FROM notes_tree WHERE note_id = ? AND parent_note_id = ?', [childNoteId, parentNoteId]);
|
const existing = await getExistingNoteTree(parentNoteId, childNoteId);
|
||||||
|
|
||||||
if (existing && !existing.is_deleted) {
|
if (existing && !existing.is_deleted) {
|
||||||
return res.send({
|
return res.send({
|
||||||
@@ -102,7 +154,7 @@ router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, async (req
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await checkCycle(parentNoteId, childNoteId)) {
|
if (!await checkTreeCycle(parentNoteId, childNoteId)) {
|
||||||
return res.send({
|
return res.send({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Cloning note here would create cycle.'
|
message: 'Cloning note here would create cycle.'
|
||||||
@@ -131,9 +183,7 @@ router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, async (req
|
|||||||
await sql.execute("UPDATE notes_tree SET is_expanded = 1 WHERE note_id = ?", [parentNoteId]);
|
await sql.execute("UPDATE notes_tree SET is_expanded = 1 WHERE note_id = ?", [parentNoteId]);
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send({
|
res.send({ success: true });
|
||||||
success: true
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
||||||
@@ -147,14 +197,7 @@ router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, async (re
|
|||||||
return res.status(500).send("After note " + afterNoteTreeId + " doesn't exist.");
|
return res.status(500).send("After note " + afterNoteTreeId + " doesn't exist.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await checkCycle(afterNote.parent_note_id, noteId)) {
|
const existing = await getExistingNoteTree(afterNote.parent_note_id, noteId);
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'Cloning note here would create cycle.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const existing = await sql.getFirstValue('SELECT * FROM notes_tree WHERE note_id = ? AND parent_note_id = ?', [noteId, afterNote.parent_note_id]);
|
|
||||||
|
|
||||||
if (existing && !existing.is_deleted) {
|
if (existing && !existing.is_deleted) {
|
||||||
return res.send({
|
return res.send({
|
||||||
@@ -163,6 +206,13 @@ router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, async (re
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!await checkTreeCycle(afterNote.parent_note_id, noteId)) {
|
||||||
|
return res.send({
|
||||||
|
success: false,
|
||||||
|
message: 'Cloning note here would create cycle.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
// we don't change date_modified so other changes are prioritized in case of conflict
|
// we don't change date_modified so other changes are prioritized in case of conflict
|
||||||
// also we would have to sync all those modified note trees otherwise hash checks would fail
|
// also we would have to sync all those modified note trees otherwise hash checks would fail
|
||||||
@@ -186,29 +236,55 @@ router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, async (re
|
|||||||
await sync_table.addNoteTreeSync(noteTree.note_tree_id, sourceId);
|
await sync_table.addNoteTreeSync(noteTree.note_tree_id, sourceId);
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send({
|
res.send({ success: true });
|
||||||
success: true
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function checkCycle(parentNoteId, childNoteId) {
|
async function loadSubTreeNoteIds(parentNoteId, subTreeNoteIds) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getExistingNoteTree(parentNoteId, childNoteId) {
|
||||||
|
return await sql.getFirst('SELECT * FROM notes_tree WHERE note_id = ? AND parent_note_id = ?', [childNoteId, parentNoteId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tree cycle can be created when cloning or when moving existing clone. This method should detect both cases.
|
||||||
|
*/
|
||||||
|
async function checkTreeCycle(parentNoteId, childNoteId) {
|
||||||
|
const subTreeNoteIds = [];
|
||||||
|
|
||||||
|
// we'll load the whole sub tree - because the cycle can start in one of the notes in the sub tree
|
||||||
|
await loadSubTreeNoteIds(childNoteId, subTreeNoteIds);
|
||||||
|
|
||||||
|
async function checkTreeCycleInner(parentNoteId) {
|
||||||
if (parentNoteId === 'root') {
|
if (parentNoteId === 'root') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parentNoteId === childNoteId) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentNoteIds = await sql.getFirstColumn("SELECT DISTINCT parent_note_id FROM notes_tree WHERE note_id = ?", [parentNoteId]);
|
const parentNoteIds = await sql.getFirstColumn("SELECT DISTINCT parent_note_id FROM notes_tree WHERE note_id = ?", [parentNoteId]);
|
||||||
|
|
||||||
for (const pid of parentNoteIds) {
|
for (const pid of parentNoteIds) {
|
||||||
if (!await checkCycle(pid, childNoteId)) {
|
if (!await checkTreeCycleInner(pid)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await checkTreeCycleInner(parentNoteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
router.put('/:noteTreeId/expanded/:expanded', auth.checkApiAuth, async (req, res, next) => {
|
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({});
|
res.send({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/force-note-sync/:noteId', auth.checkApiAuth, async (req, res, next) => {
|
||||||
|
const noteId = req.params.noteId;
|
||||||
|
|
||||||
|
await sql.doInTransaction(async () => {
|
||||||
|
await sync_table.addNoteSync(noteId);
|
||||||
|
|
||||||
|
for (const noteTreeId of await sql.getFirstColumn("SELECT note_tree_id FROM notes_tree WHERE is_deleted = 0 AND note_id = ?", [noteId])) {
|
||||||
|
await sync_table.addNoteTreeSync(noteTreeId);
|
||||||
|
await sync_table.addRecentNoteSync(noteTreeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const noteHistoryId of await sql.getFirstColumn("SELECT note_history_id FROM notes_history WHERE note_id = ?", [noteId])) {
|
||||||
|
await sync_table.addNoteHistorySync(noteHistoryId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("Forcing note sync for " + noteId);
|
||||||
|
|
||||||
|
// not awaiting for the job to finish (will probably take a long time)
|
||||||
|
sync.sync();
|
||||||
|
|
||||||
|
res.send({});
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/changed', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/changed', auth.checkApiAuth, async (req, res, next) => {
|
||||||
const lastSyncId = parseInt(req.query.lastSyncId);
|
const lastSyncId = parseInt(req.query.lastSyncId);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
const build = require('./build');
|
const build = require('./build');
|
||||||
const packageJson = require('../package');
|
const packageJson = require('../package');
|
||||||
|
|
||||||
const APP_DB_VERSION = 60;
|
const APP_DB_VERSION = 61;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
app_version: packageJson.version,
|
app_version: packageJson.version,
|
||||||
|
|||||||
@@ -3,12 +3,9 @@
|
|||||||
const migration = require('./migration');
|
const migration = require('./migration');
|
||||||
const sql = require('./sql');
|
const sql = require('./sql');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const options = require('./options');
|
|
||||||
|
|
||||||
async function checkAuth(req, res, next) {
|
async function checkAuth(req, res, next) {
|
||||||
const username = await options.getOption('username');
|
if (!await sql.isUserInitialized()) {
|
||||||
|
|
||||||
if (!username) {
|
|
||||||
res.redirect("setup");
|
res.redirect("setup");
|
||||||
}
|
}
|
||||||
else if (!req.session.loggedIn && !utils.isElectron()) {
|
else if (!req.session.loggedIn && !utils.isElectron()) {
|
||||||
@@ -53,9 +50,7 @@ async function checkApiAuthForMigrationPage(req, res, next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkAppNotInitialized(req, res, next) {
|
async function checkAppNotInitialized(req, res, next) {
|
||||||
const username = await options.getOption('username');
|
if (await sql.isUserInitialized()) {
|
||||||
|
|
||||||
if (username) {
|
|
||||||
res.status(400).send("App already initialized.");
|
res.status(400).send("App already initialized.");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
module.exports = { build_date:"2017-12-26T19:57:44-05:00", build_revision: "baab7454626b154b43144c1a07e1962ab083bde2" };
|
module.exports = { build_date:"2018-01-02T22:46:50-05:00", build_revision: "96a44a9a0c1b90ba7b58ef37a52cadbaffdf918d" };
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ const ini = require('ini');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const dataDir = require('./data_dir');
|
const dataDir = require('./data_dir');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const resource_dir = require('./resource_dir');
|
||||||
|
|
||||||
const configSampleFilePath = path.resolve(__dirname, "..", "config-sample.ini");
|
const configSampleFilePath = path.resolve(resource_dir.RESOURCE_DIR, "config-sample.ini");
|
||||||
|
|
||||||
const configFilePath = dataDir.TRILIUM_DATA_DIR + '/config.ini';
|
const configFilePath = dataDir.TRILIUM_DATA_DIR + '/config.ini';
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,46 @@ async function runCheck(query, errorText, errorList) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkTreeCycles(errorList) {
|
||||||
|
const childToParents = {};
|
||||||
|
const rows = await sql.getAll("SELECT note_id, parent_note_id FROM notes_tree");
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
const childNoteId = row.note_id;
|
||||||
|
const parentNoteId = row.parent_note_id;
|
||||||
|
|
||||||
|
if (!childToParents[childNoteId]) {
|
||||||
|
childToParents[childNoteId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
childToParents[childNoteId].push(parentNoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkTreeCycle(noteId, path, errorList) {
|
||||||
|
if (noteId === 'root') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const parentNoteId of childToParents[noteId]) {
|
||||||
|
if (path.includes(parentNoteId)) {
|
||||||
|
errorList.push(`Tree cycle detected at parent-child relationship: ${parentNoteId} - ${noteId}, whole path: ${path}`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const newPath = path.slice();
|
||||||
|
newPath.push(noteId);
|
||||||
|
|
||||||
|
checkTreeCycle(parentNoteId, newPath, errorList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const noteIds = Object.keys(childToParents);
|
||||||
|
|
||||||
|
for (const noteId of noteIds) {
|
||||||
|
checkTreeCycle(noteId, [], errorList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function runSyncRowChecks(table, key, errorList) {
|
async function runSyncRowChecks(table, key, errorList) {
|
||||||
await runCheck(`
|
await runCheck(`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -43,6 +83,8 @@ async function runSyncRowChecks(table, key, errorList) {
|
|||||||
async function runChecks() {
|
async function runChecks() {
|
||||||
const errorList = [];
|
const errorList = [];
|
||||||
|
|
||||||
|
const startTime = new Date();
|
||||||
|
|
||||||
await runCheck(`
|
await runCheck(`
|
||||||
SELECT
|
SELECT
|
||||||
note_id
|
note_id
|
||||||
@@ -124,11 +166,21 @@ async function runChecks() {
|
|||||||
await runSyncRowChecks("notes_tree", "note_tree_id", errorList);
|
await runSyncRowChecks("notes_tree", "note_tree_id", errorList);
|
||||||
await runSyncRowChecks("recent_notes", "note_tree_id", errorList);
|
await runSyncRowChecks("recent_notes", "note_tree_id", errorList);
|
||||||
|
|
||||||
|
if (errorList.length === 0) {
|
||||||
|
// we run this only if basic checks passed since this assumes basic data consistency
|
||||||
|
|
||||||
|
await checkTreeCycles(errorList);
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsedTimeMs = new Date().getTime() - startTime.getTime();
|
||||||
|
|
||||||
if (errorList.length > 0) {
|
if (errorList.length > 0) {
|
||||||
|
log.info(`Consistency checks failed (took ${elapsedTimeMs}ms) with these errors: ` + JSON.stringify(errorList));
|
||||||
|
|
||||||
messaging.sendMessageToAllClients({type: 'consistency-checks-failed'});
|
messaging.sendMessageToAllClients({type: 'consistency-checks-failed'});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.info("All consistency checks passed.");
|
log.info(`All consistency checks passed (took ${elapsedTimeMs}ms)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const sql = require('./sql');
|
const sql = require('./sql');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const options = require('./options');
|
const options = require('./options');
|
||||||
|
const log = require('./log');
|
||||||
|
|
||||||
function getHash(rows) {
|
function getHash(rows) {
|
||||||
let hash = '';
|
let hash = '';
|
||||||
@@ -13,9 +14,11 @@ function getHash(rows) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getHashes() {
|
async function getHashes() {
|
||||||
|
const startTime = new Date();
|
||||||
|
|
||||||
const optionsQuestionMarks = Array(options.SYNCED_OPTIONS.length).fill('?').join(',');
|
const optionsQuestionMarks = Array(options.SYNCED_OPTIONS.length).fill('?').join(',');
|
||||||
|
|
||||||
return {
|
const hashes = {
|
||||||
notes: getHash(await sql.getAll(`SELECT
|
notes: getHash(await sql.getAll(`SELECT
|
||||||
note_id,
|
note_id,
|
||||||
note_title,
|
note_title,
|
||||||
@@ -62,6 +65,12 @@ async function getHashes() {
|
|||||||
WHERE opt_name IN (${optionsQuestionMarks})
|
WHERE opt_name IN (${optionsQuestionMarks})
|
||||||
ORDER BY opt_name`, options.SYNCED_OPTIONS))
|
ORDER BY opt_name`, options.SYNCED_OPTIONS))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const elapseTimeMs = new Date().getTime() - startTime.getTime();
|
||||||
|
|
||||||
|
log.info(`Content hash computation took ${elapseTimeMs}ms`);
|
||||||
|
|
||||||
|
return hashes;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
const log = require('./log');
|
||||||
|
|
||||||
function arraysIdentical(a, b) {
|
function arraysIdentical(a, b) {
|
||||||
let i = a.length;
|
let i = a.length;
|
||||||
@@ -72,7 +73,15 @@ function decrypt(key, iv, cipherText) {
|
|||||||
function decryptString(dataKey, iv, cipherText) {
|
function decryptString(dataKey, iv, cipherText) {
|
||||||
const buffer = decrypt(dataKey, iv, cipherText);
|
const buffer = decrypt(dataKey, iv, cipherText);
|
||||||
|
|
||||||
return buffer.toString('utf-8');
|
const str = buffer.toString('utf-8');
|
||||||
|
|
||||||
|
if (str === 'false') {
|
||||||
|
log.error("Could not decrypt string. Buffer: " + buffer);
|
||||||
|
|
||||||
|
throw new Error("Could not decrypt string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
function noteTitleIv(iv) {
|
function noteTitleIv(iv) {
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ async function sendMessage(client, message) {
|
|||||||
async function sendMessageToAllClients(message) {
|
async function sendMessageToAllClients(message) {
|
||||||
const jsonStr = JSON.stringify(message);
|
const jsonStr = JSON.stringify(message);
|
||||||
|
|
||||||
|
log.info("Sending message to all clients: " + jsonStr);
|
||||||
|
|
||||||
webSocketServer.clients.forEach(function each(client) {
|
webSocketServer.clients.forEach(function each(client) {
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
client.send(jsonStr);
|
client.send(jsonStr);
|
||||||
|
|||||||
@@ -3,14 +3,7 @@ const sql = require('./sql');
|
|||||||
const options = require('./options');
|
const options = require('./options');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const log = require('./log');
|
const log = require('./log');
|
||||||
const path = require('path');
|
const resource_dir = require('./resource_dir');
|
||||||
|
|
||||||
const MIGRATIONS_DIR = path.resolve(__dirname, "..", "migrations");
|
|
||||||
|
|
||||||
if (!fs.existsSync(MIGRATIONS_DIR)) {
|
|
||||||
log.error("Could not find migration directory: " + MIGRATIONS_DIR);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function migrate() {
|
async function migrate() {
|
||||||
const migrations = [];
|
const migrations = [];
|
||||||
@@ -20,7 +13,7 @@ async function migrate() {
|
|||||||
|
|
||||||
const currentDbVersion = parseInt(await options.getOption('db_version'));
|
const currentDbVersion = parseInt(await options.getOption('db_version'));
|
||||||
|
|
||||||
fs.readdirSync(MIGRATIONS_DIR).forEach(file => {
|
fs.readdirSync(resource_dir.MIGRATIONS_DIR).forEach(file => {
|
||||||
const match = file.match(/([0-9]{4})__([a-zA-Z0-9_ ]+)\.(sql|js)/);
|
const match = file.match(/([0-9]{4})__([a-zA-Z0-9_ ]+)\.(sql|js)/);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -53,7 +46,7 @@ async function migrate() {
|
|||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
if (mig.type === 'sql') {
|
if (mig.type === 'sql') {
|
||||||
const migrationSql = fs.readFileSync(MIGRATIONS_DIR + "/" + mig.file).toString('utf8');
|
const migrationSql = fs.readFileSync(resource_dir.MIGRATIONS_DIR + "/" + mig.file).toString('utf8');
|
||||||
|
|
||||||
console.log("Migration with SQL script: " + migrationSql);
|
console.log("Migration with SQL script: " + migrationSql);
|
||||||
|
|
||||||
@@ -62,7 +55,7 @@ async function migrate() {
|
|||||||
else if (mig.type === 'js') {
|
else if (mig.type === 'js') {
|
||||||
console.log("Migration with JS module");
|
console.log("Migration with JS module");
|
||||||
|
|
||||||
const migrationModule = require("../" + MIGRATIONS_DIR + "/" + mig.file);
|
const migrationModule = require("../" + resource_dir.MIGRATIONS_DIR + "/" + mig.file);
|
||||||
await migrationModule(db);
|
await migrationModule(db);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
25
services/resource_dir.js
Normal file
25
services/resource_dir.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
const log = require('./log');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const RESOURCE_DIR = path.resolve(__dirname, "..");
|
||||||
|
|
||||||
|
const MIGRATIONS_DIR = path.resolve(RESOURCE_DIR, "migrations");
|
||||||
|
|
||||||
|
if (!fs.existsSync(MIGRATIONS_DIR)) {
|
||||||
|
log.error("Could not find migration directory: " + MIGRATIONS_DIR);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DB_INIT_DIR = path.resolve(RESOURCE_DIR, "db");
|
||||||
|
|
||||||
|
if (!fs.existsSync(DB_INIT_DIR)) {
|
||||||
|
log.error("Could not find DB initialization directory: " + DB_INIT_DIR);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
RESOURCE_DIR,
|
||||||
|
MIGRATIONS_DIR,
|
||||||
|
DB_INIT_DIR
|
||||||
|
};
|
||||||
@@ -4,8 +4,8 @@ const log = require('./log');
|
|||||||
const dataDir = require('./data_dir');
|
const dataDir = require('./data_dir');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const sqlite = require('sqlite');
|
const sqlite = require('sqlite');
|
||||||
const utils = require('./utils');
|
|
||||||
const app_info = require('./app_info');
|
const app_info = require('./app_info');
|
||||||
|
const resource_dir = require('./resource_dir');
|
||||||
|
|
||||||
async function createConnection() {
|
async function createConnection() {
|
||||||
return await sqlite.open(dataDir.DOCUMENT_PATH, {Promise});
|
return await sqlite.open(dataDir.DOCUMENT_PATH, {Promise});
|
||||||
@@ -28,9 +28,9 @@ const dbReady = new Promise((resolve, reject) => {
|
|||||||
if (tableResults.length !== 1) {
|
if (tableResults.length !== 1) {
|
||||||
log.info("Connected to db, but schema doesn't exist. Initializing schema ...");
|
log.info("Connected to db, but schema doesn't exist. Initializing schema ...");
|
||||||
|
|
||||||
const schema = fs.readFileSync('db/schema.sql', 'UTF-8');
|
const schema = fs.readFileSync(resource_dir.DB_INIT_DIR + '/schema.sql', 'UTF-8');
|
||||||
const notesSql = fs.readFileSync('db/main_notes.sql', 'UTF-8');
|
const notesSql = fs.readFileSync(resource_dir.DB_INIT_DIR + '/main_notes.sql', 'UTF-8');
|
||||||
const notesTreeSql = fs.readFileSync('db/main_notes_tree.sql', 'UTF-8');
|
const notesTreeSql = fs.readFileSync(resource_dir.DB_INIT_DIR + '/main_notes_tree.sql', 'UTF-8');
|
||||||
|
|
||||||
await doInTransaction(async () => {
|
await doInTransaction(async () => {
|
||||||
await executeScript(schema);
|
await executeScript(schema);
|
||||||
@@ -49,9 +49,7 @@ const dbReady = new Promise((resolve, reject) => {
|
|||||||
// the database
|
// the database
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const username = await getFirstValue("SELECT opt_value FROM options WHERE opt_name = 'username'");
|
if (!await isUserInitialized()) {
|
||||||
|
|
||||||
if (!username) {
|
|
||||||
log.info("Login/password not initialized. DB not ready.");
|
log.info("Login/password not initialized. DB not ready.");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -235,8 +233,15 @@ async function isDbUpToDate() {
|
|||||||
return upToDate;
|
return upToDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function isUserInitialized() {
|
||||||
|
const username = await getFirstValue("SELECT opt_value FROM options WHERE opt_name = 'username'");
|
||||||
|
|
||||||
|
return !!username;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
dbReady,
|
dbReady,
|
||||||
|
isUserInitialized,
|
||||||
insert,
|
insert,
|
||||||
replace,
|
replace,
|
||||||
getFirstValue,
|
getFirstValue,
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ async function addOptionsSync(optName, sourceId) {
|
|||||||
await addEntitySync("options", optName, sourceId);
|
await addEntitySync("options", optName, sourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addRecentNoteSync(notePath, sourceId) {
|
async function addRecentNoteSync(noteTreeId, sourceId) {
|
||||||
await addEntitySync("recent_notes", notePath, sourceId);
|
await addEntitySync("recent_notes", noteTreeId, sourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addEntitySync(entityName, entityId, sourceId) {
|
async function addEntitySync(entityName, entityId, sourceId) {
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ async function updateNoteHistory(entity, sourceId) {
|
|||||||
const orig = await sql.getFirstOrNull("SELECT * FROM notes_history WHERE note_history_id = ?", [entity.note_history_id]);
|
const orig = await sql.getFirstOrNull("SELECT * FROM notes_history WHERE note_history_id = ?", [entity.note_history_id]);
|
||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
if (orig === null || orig.date_modified_to < entity.date_modified_to) {
|
// we update note history even if date modified to is the same because the only thing which might have changed
|
||||||
|
// is the protected status (and correnspondingly note_title and note_text) which doesn't affect the date_modified_to
|
||||||
|
if (orig === null || orig.date_modified_to <= entity.date_modified_to) {
|
||||||
await sql.replace('notes_history', entity);
|
await sql.replace('notes_history', entity);
|
||||||
|
|
||||||
await sync_table.addNoteHistorySync(entity.note_history_id, sourceId);
|
await sync_table.addNoteHistorySync(entity.note_history_id, sourceId);
|
||||||
|
|||||||
@@ -5,9 +5,7 @@
|
|||||||
<title>Trilium Notes</title>
|
<title>Trilium Notes</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="loader-wrapper"><div id="loader"></div></div>
|
<div id="container" style="display:none;">
|
||||||
|
|
||||||
<div id="container" style="display: none;">
|
|
||||||
<div id="header" class="hide-toggle">
|
<div id="header" class="hide-toggle">
|
||||||
<div id="header-title">
|
<div id="header-title">
|
||||||
<img src="images/app-icons/png/24x24.png">
|
<img src="images/app-icons/png/24x24.png">
|
||||||
@@ -29,7 +27,7 @@
|
|||||||
|
|
||||||
<button class="btn btn-xs" onclick="settings.showDialog();">Settings</button>
|
<button class="btn btn-xs" onclick="settings.showDialog();">Settings</button>
|
||||||
|
|
||||||
<form action="logout" method="POST" style="display: inline;">
|
<form action="logout" id="logout-button" method="POST" style="display: inline;">
|
||||||
<input type="submit" class="btn btn-xs" value="Logout">
|
<input type="submit" class="btn btn-xs" value="Logout">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,7 +65,7 @@
|
|||||||
<div id="tree" class="hide-toggle" style="grid-area: tree; overflow: auto;">
|
<div id="tree" class="hide-toggle" style="grid-area: tree; overflow: auto;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="parent-list">
|
<div id="parent-list" class="hide-toggle">
|
||||||
<p><strong>Note locations:</strong></p>
|
<p><strong>Note locations:</strong></p>
|
||||||
|
|
||||||
<ul id="parent-list-list"></ul>
|
<ul id="parent-list-list"></ul>
|
||||||
@@ -172,11 +170,11 @@
|
|||||||
<div id="protected-session-password-dialog" title="Protected session" style="display: none;">
|
<div id="protected-session-password-dialog" title="Protected session" style="display: none;">
|
||||||
<form id="protected-session-password-form">
|
<form id="protected-session-password-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="protected-session-password">To proceed with requested action you need to enter protected session by entering password:</label>
|
<label for="protected-session-password">To proceed with requested action you need to start protected session by entering password:</label>
|
||||||
<input id="protected-session-password" class="form-control" type="password">
|
<input id="protected-session-password" class="form-control" type="password">
|
||||||
</div>
|
</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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -327,7 +325,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="note-source-dialog" title="Note source" style="display: none; padding: 20px;">
|
<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>
|
||||||
|
|
||||||
<div id="tooltip" style="display: none;"></div>
|
<div id="tooltip" style="display: none;"></div>
|
||||||
@@ -399,5 +397,11 @@
|
|||||||
<script src="javascripts/link.js"></script>
|
<script src="javascripts/link.js"></script>
|
||||||
<script src="javascripts/sync.js"></script>
|
<script src="javascripts/sync.js"></script>
|
||||||
<script src="javascripts/messaging.js"></script>
|
<script src="javascripts/messaging.js"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
// we hide container initally because otherwise it is rendered first without CSS and then flickers into
|
||||||
|
// final form which is pretty ugly.
|
||||||
|
$("#container").show();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -48,6 +48,5 @@
|
|||||||
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
|
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
|
||||||
|
|
||||||
<link href="libraries/bootstrap/css/bootstrap.css" rel="stylesheet">
|
<link href="libraries/bootstrap/css/bootstrap.css" rel="stylesheet">
|
||||||
<script src="libraries/bootstrap/js/bootstrap.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user