Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad8d35efe9 | |||
| 0217b1c85d | |||
| c0aa14f586 | |||
| b54cfab4ff | |||
| a08985e7a6 | |||
| a789025025 | |||
| 3f307b117e | |||
| a232035d47 | |||
| 9d38e9342d | |||
| 4bc4b9ade7 | |||
| f0217cae5e | |||
| da050c6369 | |||
| 842c317568 | |||
| 47845930f4 | |||
| 972f2f40bf | |||
| 94111c464b | |||
| 94e18dfb7c | |||
| bc9903191e | |||
| cd8c24ceae | |||
| f9709c9c39 | |||
| c0964a4f12 | |||
| bcef8579ce | |||
| 1180be75d1 | |||
| cfa49c7b1b | |||
| 8e4926ed7f | |||
| 2430dcba65 | |||
| 7c885a8b76 | |||
| 402e29d6dc | |||
| e7faebfac3 | |||
| 10a5773c66 | |||
| 1e8472266f | |||
| b30792a3da | |||
| 8b56fb10fd | |||
| 26602e8226 | |||
| b8eeb0371c | |||
| b1c4737e78 | |||
| 3860028a9e | |||
| 16d97b95af | |||
| 20465a4f71 | |||
| 2ff6e50af4 | |||
| e0378c5064 | |||
| e29aee1aae | |||
| 1aff42f453 | |||
| a098630e09 | |||
| 074eb1c02f | |||
| 657496ea37 | |||
| 034aaa7209 | |||
| 4ceba8cc6e | |||
| 97f7fe7b18 | |||
| ab550a1e8d | |||
| 08e8047d8a | |||
| 67da877135 | |||
| 2d339dec6b | |||
| b78ab1ee02 | |||
| 2f5f116345 | |||
| 64f1671566 | |||
| e079359c15 | |||
| c4ab6b4866 | |||
| 630d9f2e45 | |||
| bbceb6251a | |||
| 89f117da5b | |||
| 61f197dd81 | |||
| 1ee2abcc42 | |||
| f4242b4096 | |||
| d59542dd6f | |||
| 211ff90ee8 | |||
| 3f40a52f65 | |||
| df4cf80be4 | |||
| bc854ee149 | |||
| b23ead8097 | |||
| 886fdf7cd6 | |||
| de20183a22 | |||
| 42b5437c87 | |||
| ae29c6bac4 | |||
| 1dce96b4c1 | |||
| db9e35a7e1 | |||
| 7a383a1314 | |||
| 5290aab781 | |||
| 86c3bbe5a2 | |||
| 21854b4a04 | |||
| 83f125a79f | |||
| e36bc42519 | |||
| 15ac81627c | |||
| 57fae2c8c6 | |||
| 87b76abef9 | |||
| 89322c4b03 | |||
| b7efc92099 | |||
| 20a187fab9 | |||
| 0b001f41c0 | |||
| 242977c7a5 | |||
| 364ac331da | |||
| fcc0a80f4e | |||
| 8996f35cc0 | |||
| 980309ae2a | |||
| 6a6bd4541a | |||
| a14aa461ca |
@@ -3,13 +3,6 @@ description: Report a bug
|
||||
title: "(Bug report) "
|
||||
labels: "Type: Bug"
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Preflight Checklist
|
||||
description: Please ensure you've completed all of the following.
|
||||
options:
|
||||
- label: I have searched the [issue tracker](https://www.github.com/zadam/trilium/issues) for a bug report that matches the one I want to file, without success.
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Trilium Version
|
||||
@@ -30,7 +23,7 @@ body:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What is your setup?
|
||||
label: What is your setup?
|
||||
description: https://github.com/zadam/trilium/wiki#choose-the-setup
|
||||
options:
|
||||
- Local (no sync)
|
||||
@@ -47,17 +40,7 @@ body:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
label: Description
|
||||
description: A clear and concise description of the bug and any additional information.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: A clear description of what actually happens.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here.
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
name: Feature Request
|
||||
description: Report a bug
|
||||
description: Ask for a new feature to be added
|
||||
title: "(Feature request) "
|
||||
labels: "Type: Enhancement"
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Preflight Checklist
|
||||
description: Please ensure you've completed all of the following.
|
||||
options:
|
||||
- label: I have searched the [issue tracker](https://www.github.com/zadam/trilium/issues) for a feature request that matches the one I want to file, without success.
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe feature
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ image:
|
||||
file: .gitpod.dockerfile
|
||||
|
||||
tasks:
|
||||
- before: nvm install 14.18.1 && nvm use 14.18.1
|
||||
- before: nvm install 16.13.1 && nvm use 16.13.1
|
||||
init: npm install
|
||||
command: npm run start-server
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM node:14.18.1-alpine
|
||||
FROM node:16.13.1-alpine
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
-1
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PKG_DIR=dist/trilium-linux-x64-server
|
||||
NODE_VERSION=14.18.1
|
||||
NODE_VERSION=16.13.1
|
||||
|
||||
if [ "$1" != "DONTCOPY" ]
|
||||
then
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ if [[ $# -eq 0 ]] ; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
n exec 14.18.1 npm run webpack
|
||||
n exec 16.13.1 npm run webpack
|
||||
|
||||
DIR=$1
|
||||
|
||||
@@ -27,7 +27,7 @@ cp -r electron.js $DIR/
|
||||
cp webpack-* $DIR/
|
||||
|
||||
# run in subshell (so we return to original dir)
|
||||
(cd $DIR && n exec 14.18.1 npm install --only=prod)
|
||||
(cd $DIR && n exec 16.13.1 npm install --only=prod)
|
||||
|
||||
# cleanup of useless files in dependencies
|
||||
rm -r $DIR/node_modules/image-q/demo
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE branches DROP COLUMN utcDateCreated;
|
||||
ALTER TABLE options DROP COLUMN utcDateCreated;
|
||||
@@ -0,0 +1,23 @@
|
||||
CREATE TABLE IF NOT EXISTS "mig_entity_changes" (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`entityName` TEXT NOT NULL,
|
||||
`entityId` TEXT NOT NULL,
|
||||
`hash` TEXT NOT NULL,
|
||||
`isErased` INT NOT NULL,
|
||||
`changeId` TEXT NOT NULL,
|
||||
`sourceId` TEXT NOT NULL,
|
||||
`isSynced` INTEGER NOT NULL,
|
||||
`utcDateChanged` TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO mig_entity_changes (entityName, entityId, hash, isErased, changeId, sourceId, isSynced, utcDateChanged)
|
||||
SELECT entityName, entityId, hash, isErased, '', sourceId, isSynced, utcDateChanged FROM entity_changes;
|
||||
|
||||
DROP TABLE entity_changes;
|
||||
|
||||
ALTER TABLE mig_entity_changes RENAME TO entity_changes;
|
||||
|
||||
CREATE UNIQUE INDEX `IDX_entityChanges_entityName_entityId` ON "entity_changes" (
|
||||
`entityName`,
|
||||
`entityId`
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
UPDATE branches SET branchId = 'hidden' where branchId = (
|
||||
SELECT branchId FROM branches
|
||||
WHERE parentNoteId = 'root'
|
||||
AND noteId = 'hidden'
|
||||
AND isDeleted = 0
|
||||
ORDER BY utcDateModified
|
||||
LIMIT 1
|
||||
);
|
||||
+1
-2
@@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS "entity_changes" (
|
||||
`entityId` TEXT NOT NULL,
|
||||
`hash` TEXT NOT NULL,
|
||||
`isErased` INT NOT NULL,
|
||||
`changeId` TEXT NOT NULL,
|
||||
`sourceId` TEXT NOT NULL,
|
||||
`isSynced` INTEGER NOT NULL,
|
||||
`utcDateChanged` TEXT NOT NULL
|
||||
@@ -24,7 +25,6 @@ CREATE TABLE IF NOT EXISTS "branches" (
|
||||
`isDeleted` INTEGER NOT NULL DEFAULT 0,
|
||||
`deleteId` TEXT DEFAULT NULL,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
utcDateCreated TEXT NOT NULL,
|
||||
PRIMARY KEY(`branchId`));
|
||||
CREATE TABLE IF NOT EXISTS "notes" (
|
||||
`noteId` TEXT NOT NULL,
|
||||
@@ -65,7 +65,6 @@ CREATE TABLE IF NOT EXISTS "options"
|
||||
name TEXT not null PRIMARY KEY,
|
||||
value TEXT,
|
||||
isSynced INTEGER default 0 not null,
|
||||
utcDateCreated TEXT not null,
|
||||
utcDateModified TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "attributes"
|
||||
|
||||
@@ -4,6 +4,7 @@ const {app, globalShortcut} = require('electron');
|
||||
const sqlInit = require('./src/services/sql_init');
|
||||
const appIconService = require('./src/services/app_icon');
|
||||
const windowService = require('./src/services/window');
|
||||
const tray = require('./src/services/tray');
|
||||
|
||||
// Adds debug features like hotkeys for triggering dev tools and reload
|
||||
require('electron-debug')();
|
||||
@@ -30,6 +31,8 @@ app.on('ready', async () => {
|
||||
await sqlInit.dbReady;
|
||||
|
||||
await windowService.createMainWindow();
|
||||
|
||||
tray.createTray();
|
||||
}
|
||||
else {
|
||||
await windowService.createSetupWindow();
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
+2
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1740
-1507
File diff suppressed because it is too large
Load Diff
+1
-1
File diff suppressed because one or more lines are too long
Binary file not shown.
+1595
-1501
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 952 KiB After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
+1
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+3
-3
File diff suppressed because one or more lines are too long
Vendored
+3
-34
File diff suppressed because one or more lines are too long
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}
|
||||
/*# sourceMappingURL=normalize.min.css.map */
|
||||
@@ -1,656 +0,0 @@
|
||||
/**
|
||||
* Springy v2.7.1
|
||||
*
|
||||
* Copyright (c) 2010-2013 Dennis Hotson
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
window.Springy = function() {
|
||||
const Springy = {};
|
||||
|
||||
const Graph = Springy.Graph = function () {
|
||||
this.nodeSet = {};
|
||||
this.nodes = [];
|
||||
this.edges = [];
|
||||
this.adjacency = {};
|
||||
|
||||
this.nextNodeId = 0;
|
||||
this.nextEdgeId = 0;
|
||||
this.eventListeners = [];
|
||||
};
|
||||
|
||||
const Node = Springy.Node = function (id, data) {
|
||||
this.id = id;
|
||||
this.data = (data !== undefined) ? data : {};
|
||||
|
||||
// Data fields used by layout algorithm in this file:
|
||||
// this.data.mass
|
||||
// Data used by default renderer in springyui.js
|
||||
// this.data.label
|
||||
};
|
||||
|
||||
const Edge = Springy.Edge = function (id, source, target, data) {
|
||||
this.id = id;
|
||||
this.source = source;
|
||||
this.target = target;
|
||||
this.data = (data !== undefined) ? data : {};
|
||||
|
||||
// Edge data field used by layout alorithm
|
||||
// this.data.length
|
||||
// this.data.type
|
||||
};
|
||||
|
||||
Graph.prototype.addNode = function(node) {
|
||||
if (!(node.id in this.nodeSet)) {
|
||||
this.nodes.push(node);
|
||||
}
|
||||
|
||||
this.nodeSet[node.id] = node;
|
||||
|
||||
this.notify();
|
||||
return node;
|
||||
};
|
||||
|
||||
Graph.prototype.addNodes = function() {
|
||||
// accepts variable number of arguments, where each argument
|
||||
// is a string that becomes both node identifier and label
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
const name = arguments[i];
|
||||
const node = new Node(name, {label: name});
|
||||
this.addNode(node);
|
||||
}
|
||||
};
|
||||
|
||||
Graph.prototype.addEdge = function(edge) {
|
||||
let exists = false;
|
||||
this.edges.forEach(function(e) {
|
||||
if (edge.id === e.id) { exists = true; }
|
||||
});
|
||||
|
||||
if (!exists) {
|
||||
this.edges.push(edge);
|
||||
}
|
||||
|
||||
if (!(edge.source.id in this.adjacency)) {
|
||||
this.adjacency[edge.source.id] = {};
|
||||
}
|
||||
if (!(edge.target.id in this.adjacency[edge.source.id])) {
|
||||
this.adjacency[edge.source.id][edge.target.id] = [];
|
||||
}
|
||||
|
||||
exists = false;
|
||||
this.adjacency[edge.source.id][edge.target.id].forEach(function(e) {
|
||||
if (edge.id === e.id) { exists = true; }
|
||||
});
|
||||
|
||||
if (!exists) {
|
||||
this.adjacency[edge.source.id][edge.target.id].push(edge);
|
||||
}
|
||||
|
||||
this.notify();
|
||||
return edge;
|
||||
};
|
||||
|
||||
Graph.prototype.addEdges = function() {
|
||||
// accepts variable number of arguments, where each argument
|
||||
// is a triple [nodeid1, nodeid2, attributes]
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
const e = arguments[i];
|
||||
const node1 = this.nodeSet[e[0]];
|
||||
if (node1 == undefined) {
|
||||
throw new TypeError("invalid node name: " + e[0]);
|
||||
}
|
||||
const node2 = this.nodeSet[e[1]];
|
||||
if (node2 == undefined) {
|
||||
throw new TypeError("invalid node name: " + e[1]);
|
||||
}
|
||||
const attr = e[2];
|
||||
|
||||
this.newEdge(node1, node2, attr);
|
||||
}
|
||||
};
|
||||
|
||||
Graph.prototype.newNode = function(data) {
|
||||
const node = new Node(this.nextNodeId++, data);
|
||||
this.addNode(node);
|
||||
return node;
|
||||
};
|
||||
|
||||
Graph.prototype.newEdge = function(source, target, data) {
|
||||
const edge = new Edge(this.nextEdgeId++, source, target, data);
|
||||
this.addEdge(edge);
|
||||
return edge;
|
||||
};
|
||||
|
||||
|
||||
// add nodes and edges from JSON object
|
||||
Graph.prototype.loadJSON = function(json) {
|
||||
/**
|
||||
Springy's simple JSON format for graphs.
|
||||
|
||||
historically, Springy uses separate lists
|
||||
of nodes and edges:
|
||||
|
||||
{
|
||||
"nodes": [
|
||||
"center",
|
||||
"left",
|
||||
"right",
|
||||
"up",
|
||||
"satellite"
|
||||
],
|
||||
"edges": [
|
||||
["center", "left"],
|
||||
["center", "right"],
|
||||
["center", "up"]
|
||||
]
|
||||
}
|
||||
|
||||
**/
|
||||
// parse if a string is passed (EC5+ browsers)
|
||||
if (typeof json == 'string' || json instanceof String) {
|
||||
json = JSON.parse( json );
|
||||
}
|
||||
|
||||
if ('nodes' in json || 'edges' in json) {
|
||||
this.addNodes.apply(this, json['nodes']);
|
||||
this.addEdges.apply(this, json['edges']);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// find the edges from node1 to node2
|
||||
Graph.prototype.getEdges = function(node1, node2) {
|
||||
if (node1.id in this.adjacency
|
||||
&& node2.id in this.adjacency[node1.id]) {
|
||||
return this.adjacency[node1.id][node2.id];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
// remove a node and it's associated edges from the graph
|
||||
Graph.prototype.removeNode = function(node) {
|
||||
if (node.id in this.nodeSet) {
|
||||
delete this.nodeSet[node.id];
|
||||
}
|
||||
|
||||
for (let i = this.nodes.length - 1; i >= 0; i--) {
|
||||
if (this.nodes[i].id === node.id) {
|
||||
this.nodes.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.detachNode(node);
|
||||
};
|
||||
|
||||
// removes edges associated with a given node
|
||||
Graph.prototype.detachNode = function(node) {
|
||||
const tmpEdges = this.edges.slice();
|
||||
tmpEdges.forEach(function(e) {
|
||||
if (e.source.id === node.id || e.target.id === node.id) {
|
||||
this.removeEdge(e);
|
||||
}
|
||||
}, this);
|
||||
|
||||
this.notify();
|
||||
};
|
||||
|
||||
// remove a node and it's associated edges from the graph
|
||||
Graph.prototype.removeEdge = function(edge) {
|
||||
for (let i = this.edges.length - 1; i >= 0; i--) {
|
||||
if (this.edges[i].id === edge.id) {
|
||||
this.edges.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (const x in this.adjacency) {
|
||||
for (const y in this.adjacency[x]) {
|
||||
const edges = this.adjacency[x][y];
|
||||
|
||||
for (let j = edges.length - 1; j >= 0; j--) {
|
||||
if (this.adjacency[x][y][j].id === edge.id) {
|
||||
this.adjacency[x][y].splice(j, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up empty edge arrays
|
||||
if (this.adjacency[x][y].length == 0) {
|
||||
delete this.adjacency[x][y];
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up empty objects
|
||||
if (isEmpty(this.adjacency[x])) {
|
||||
delete this.adjacency[x];
|
||||
}
|
||||
}
|
||||
|
||||
this.notify();
|
||||
};
|
||||
|
||||
/* Merge a list of nodes and edges into the current graph. eg.
|
||||
var o = {
|
||||
nodes: [
|
||||
{id: 123, data: {type: 'user', userid: 123, displayname: 'aaa'}},
|
||||
{id: 234, data: {type: 'user', userid: 234, displayname: 'bbb'}}
|
||||
],
|
||||
edges: [
|
||||
{from: 0, to: 1, type: 'submitted_design', directed: true, data: {weight: }}
|
||||
]
|
||||
}
|
||||
*/
|
||||
Graph.prototype.merge = function(data) {
|
||||
const nodes = [];
|
||||
data.nodes.forEach(function(n) {
|
||||
nodes.push(this.addNode(new Node(n.id, n.data)));
|
||||
}, this);
|
||||
|
||||
data.edges.forEach(function(e) {
|
||||
const from = nodes[e.from];
|
||||
const to = nodes[e.to];
|
||||
|
||||
let id = (e.directed)
|
||||
? (e.type + "-" + from.id + "-" + to.id)
|
||||
: (from.id < to.id) // normalise id for non-directed edges
|
||||
? e.type + "-" + from.id + "-" + to.id
|
||||
: e.type + "-" + to.id + "-" + from.id;
|
||||
|
||||
const edge = this.addEdge(new Edge(id, from, to, e.data));
|
||||
edge.data.type = e.type;
|
||||
}, this);
|
||||
};
|
||||
|
||||
Graph.prototype.filterNodes = function(fn) {
|
||||
const tmpNodes = this.nodes.slice();
|
||||
tmpNodes.forEach(function(n) {
|
||||
if (!fn(n)) {
|
||||
this.removeNode(n);
|
||||
}
|
||||
}, this);
|
||||
};
|
||||
|
||||
Graph.prototype.filterEdges = function(fn) {
|
||||
const tmpEdges = this.edges.slice();
|
||||
tmpEdges.forEach(function(e) {
|
||||
if (!fn(e)) {
|
||||
this.removeEdge(e);
|
||||
}
|
||||
}, this);
|
||||
};
|
||||
|
||||
|
||||
Graph.prototype.addGraphListener = function(obj) {
|
||||
this.eventListeners.push(obj);
|
||||
};
|
||||
|
||||
Graph.prototype.notify = function() {
|
||||
this.eventListeners.forEach(function(obj){
|
||||
obj.graphChanged();
|
||||
});
|
||||
};
|
||||
|
||||
// -----------
|
||||
const Layout = Springy.Layout = {};
|
||||
Layout.ForceDirected = function(graph, stopCheckerCallback, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed) {
|
||||
this.graph = graph;
|
||||
this.stopCheckerCallback = stopCheckerCallback || (() => false);
|
||||
this.stiffness = stiffness; // spring stiffness constant
|
||||
this.repulsion = repulsion; // repulsion constant
|
||||
this.damping = damping; // velocity damping factor
|
||||
this.minEnergyThreshold = minEnergyThreshold || 0.01; //threshold used to determine render stop
|
||||
this.maxSpeed = maxSpeed || Infinity; // nodes aren't allowed to exceed this speed
|
||||
|
||||
this.nodePoints = {}; // keep track of points associated with nodes
|
||||
this.edgeSprings = {}; // keep track of springs associated with edges
|
||||
};
|
||||
|
||||
Layout.ForceDirected.prototype.point = function(node) {
|
||||
if (!(node.id in this.nodePoints)) {
|
||||
const mass = (node.data.mass !== undefined) ? node.data.mass : 1.0;
|
||||
this.nodePoints[node.id] = new Layout.ForceDirected.Point(Vector.random(), mass);
|
||||
}
|
||||
|
||||
return this.nodePoints[node.id];
|
||||
};
|
||||
|
||||
Layout.ForceDirected.prototype.spring = function(edge) {
|
||||
if (!(edge.id in this.edgeSprings)) {
|
||||
const length = (edge.data.length !== undefined) ? edge.data.length : 1.0;
|
||||
|
||||
let existingSpring = false;
|
||||
|
||||
const from = this.graph.getEdges(edge.source, edge.target);
|
||||
from.forEach(function(e) {
|
||||
if (existingSpring === false && e.id in this.edgeSprings) {
|
||||
existingSpring = this.edgeSprings[e.id];
|
||||
}
|
||||
}, this);
|
||||
|
||||
if (existingSpring !== false) {
|
||||
return new Layout.ForceDirected.Spring(existingSpring.point1, existingSpring.point2, 0.0, 0.0);
|
||||
}
|
||||
|
||||
const to = this.graph.getEdges(edge.target, edge.source);
|
||||
to.forEach(function(e){
|
||||
if (existingSpring === false && e.id in this.edgeSprings) {
|
||||
existingSpring = this.edgeSprings[e.id];
|
||||
}
|
||||
}, this);
|
||||
|
||||
if (existingSpring !== false) {
|
||||
return new Layout.ForceDirected.Spring(existingSpring.point2, existingSpring.point1, 0.0, 0.0);
|
||||
}
|
||||
|
||||
this.edgeSprings[edge.id] = new Layout.ForceDirected.Spring(
|
||||
this.point(edge.source), this.point(edge.target), length, this.stiffness
|
||||
);
|
||||
}
|
||||
|
||||
return this.edgeSprings[edge.id];
|
||||
};
|
||||
|
||||
// callback should accept two arguments: Node, Point
|
||||
Layout.ForceDirected.prototype.eachNode = function(callback) {
|
||||
const t = this;
|
||||
this.graph.nodes.forEach(function(n){
|
||||
callback.call(t, n, t.point(n));
|
||||
});
|
||||
};
|
||||
|
||||
// callback should accept two arguments: Edge, Spring
|
||||
Layout.ForceDirected.prototype.eachEdge = function(callback) {
|
||||
const t = this;
|
||||
this.graph.edges.forEach(function(e){
|
||||
callback.call(t, e, t.spring(e));
|
||||
});
|
||||
};
|
||||
|
||||
// callback should accept one argument: Spring
|
||||
Layout.ForceDirected.prototype.eachSpring = function(callback) {
|
||||
const t = this;
|
||||
this.graph.edges.forEach(function(e){
|
||||
callback.call(t, t.spring(e));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Physics stuff
|
||||
Layout.ForceDirected.prototype.applyCoulombsLaw = function() {
|
||||
this.eachNode(function(n1, point1) {
|
||||
this.eachNode(function(n2, point2) {
|
||||
if (point1 !== point2) {
|
||||
const d = point1.p.subtract(point2.p);
|
||||
const distance = d.magnitude() + 0.1; // avoid massive forces at small distances (and divide by zero)
|
||||
|
||||
const direction = d.normalise();
|
||||
|
||||
// apply force to each end point
|
||||
point1.applyForce(direction.multiply(this.repulsion).divide(distance * distance * distance * 0.5));
|
||||
point2.applyForce(direction.multiply(this.repulsion).divide(distance * distance * distance * -0.5));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Layout.ForceDirected.prototype.applyHookesLaw = function() {
|
||||
this.eachSpring(function(spring){
|
||||
const d = spring.point2.p.subtract(spring.point1.p); // the direction of the spring
|
||||
const displacement = spring.length - d.magnitude();
|
||||
const direction = d.normalise();
|
||||
|
||||
// apply force to each end point
|
||||
spring.point1.applyForce(direction.multiply(spring.k * displacement * -0.5));
|
||||
spring.point2.applyForce(direction.multiply(spring.k * displacement * 0.5));
|
||||
});
|
||||
};
|
||||
|
||||
Layout.ForceDirected.prototype.attractToCentre = function() {
|
||||
this.eachNode(function(node, point) {
|
||||
const direction = point.p.multiply(-1.0);
|
||||
point.applyForce(direction.multiply(this.repulsion / 50.0));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Layout.ForceDirected.prototype.updateVelocity = function(timestep) {
|
||||
this.eachNode(function(node, point) {
|
||||
// Is this, along with updatePosition below, the only places that your
|
||||
// integration code exist?
|
||||
point.v = point.v.add(point.a.multiply(timestep)).multiply(this.damping);
|
||||
if (point.v.magnitude() > this.maxSpeed) {
|
||||
point.v = point.v.normalise().multiply(this.maxSpeed);
|
||||
}
|
||||
point.a = new Vector(0,0);
|
||||
});
|
||||
};
|
||||
|
||||
Layout.ForceDirected.prototype.updatePosition = function(timestep) {
|
||||
this.eachNode(function(node, point) {
|
||||
// Same question as above; along with updateVelocity, is this all of
|
||||
// your integration code?
|
||||
point.p = point.p.add(point.v.multiply(timestep));
|
||||
});
|
||||
};
|
||||
|
||||
// Calculate the total kinetic energy of the system
|
||||
Layout.ForceDirected.prototype.totalEnergy = function(timestep) {
|
||||
let energy = 0.0;
|
||||
this.eachNode(function(node, point) {
|
||||
const speed = point.v.magnitude();
|
||||
energy += 0.5 * point.m * speed * speed;
|
||||
});
|
||||
|
||||
return energy;
|
||||
};
|
||||
|
||||
/**
|
||||
* Start simulation if it's not running already.
|
||||
* In case it's running then the call is ignored, and none of the callbacks passed is ever executed.
|
||||
*/
|
||||
Layout.ForceDirected.prototype.start = function(onRenderStop) {
|
||||
const t = this;
|
||||
|
||||
if (this._started) return;
|
||||
this._started = true;
|
||||
this._stop = false;
|
||||
|
||||
function step() {
|
||||
t.tick(0.03);
|
||||
|
||||
if (t.stopCheckerCallback()) {
|
||||
onRenderStop();
|
||||
}
|
||||
|
||||
// stop simulation when energy of the system goes below a threshold
|
||||
if (t._stop || t.totalEnergy() < t.minEnergyThreshold) {
|
||||
t._started = false;
|
||||
onRenderStop();
|
||||
} else {
|
||||
requestIdleCallback(step, { timeout: 30 });
|
||||
}
|
||||
}
|
||||
|
||||
step();
|
||||
};
|
||||
|
||||
Layout.ForceDirected.prototype.stop = function() {
|
||||
this._stop = true;
|
||||
};
|
||||
|
||||
Layout.ForceDirected.prototype.tick = function(timestep) {
|
||||
this.applyCoulombsLaw();
|
||||
this.applyHookesLaw();
|
||||
this.attractToCentre();
|
||||
this.updateVelocity(timestep);
|
||||
this.updatePosition(timestep);
|
||||
};
|
||||
|
||||
// Find the nearest point to a particular position
|
||||
Layout.ForceDirected.prototype.nearest = function(pos) {
|
||||
let min = {node: null, point: null, distance: null};
|
||||
const t = this;
|
||||
this.graph.nodes.forEach(function(n){
|
||||
const point = t.point(n);
|
||||
const distance = point.p.subtract(pos).magnitude();
|
||||
|
||||
if (min.distance === null || distance < min.distance) {
|
||||
min = {node: n, point: point, distance: distance};
|
||||
}
|
||||
});
|
||||
|
||||
return min;
|
||||
};
|
||||
|
||||
// returns [bottomleft, topright]
|
||||
Layout.ForceDirected.prototype.getBoundingBox = function() {
|
||||
const bottomleft = new Vector(-2, -2);
|
||||
const topright = new Vector(2, 2);
|
||||
|
||||
this.eachNode(function(n, point) {
|
||||
if (point.p.x < bottomleft.x) {
|
||||
bottomleft.x = point.p.x;
|
||||
}
|
||||
if (point.p.y < bottomleft.y) {
|
||||
bottomleft.y = point.p.y;
|
||||
}
|
||||
if (point.p.x > topright.x) {
|
||||
topright.x = point.p.x;
|
||||
}
|
||||
if (point.p.y > topright.y) {
|
||||
topright.y = point.p.y;
|
||||
}
|
||||
});
|
||||
|
||||
const padding = topright.subtract(bottomleft).multiply(0.07); // ~5% padding
|
||||
|
||||
return {bottomleft: bottomleft.subtract(padding), topright: topright.add(padding)};
|
||||
};
|
||||
|
||||
|
||||
// Vector
|
||||
const Vector = Springy.Vector = function(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
};
|
||||
|
||||
Vector.random = function() {
|
||||
return new Vector(10.0 * (Math.random() - 0.5), 10.0 * (Math.random() - 0.5));
|
||||
};
|
||||
|
||||
Vector.prototype.add = function(v2) {
|
||||
return new Vector(this.x + v2.x, this.y + v2.y);
|
||||
};
|
||||
|
||||
Vector.prototype.subtract = function(v2) {
|
||||
return new Vector(this.x - v2.x, this.y - v2.y);
|
||||
};
|
||||
|
||||
Vector.prototype.multiply = function(n) {
|
||||
return new Vector(this.x * n, this.y * n);
|
||||
};
|
||||
|
||||
Vector.prototype.divide = function(n) {
|
||||
return new Vector((this.x / n) || 0, (this.y / n) || 0); // Avoid divide by zero errors..
|
||||
};
|
||||
|
||||
Vector.prototype.magnitude = function() {
|
||||
return Math.sqrt(this.x*this.x + this.y*this.y);
|
||||
};
|
||||
|
||||
Vector.prototype.normal = function() {
|
||||
return new Vector(-this.y, this.x);
|
||||
};
|
||||
|
||||
Vector.prototype.normalise = function() {
|
||||
return this.divide(this.magnitude());
|
||||
};
|
||||
|
||||
// Point
|
||||
Layout.ForceDirected.Point = function(position, mass) {
|
||||
this.p = position; // position
|
||||
this.m = mass; // mass
|
||||
this.v = new Vector(0, 0); // velocity
|
||||
this.a = new Vector(0, 0); // acceleration
|
||||
};
|
||||
|
||||
Layout.ForceDirected.Point.prototype.applyForce = function(force) {
|
||||
this.a = this.a.add(force.divide(this.m));
|
||||
};
|
||||
|
||||
// Spring
|
||||
Layout.ForceDirected.Spring = function(point1, point2, length, k) {
|
||||
this.point1 = point1;
|
||||
this.point2 = point2;
|
||||
this.length = length; // spring length at rest
|
||||
this.k = k; // spring constant (See Hooke's law) .. how stiff the spring is
|
||||
};
|
||||
|
||||
// Layout.ForceDirected.Spring.prototype.distanceToPoint = function(point)
|
||||
// {
|
||||
// // hardcore vector arithmetic.. ohh yeah!
|
||||
// // .. see http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment/865080#865080
|
||||
// var n = this.point2.p.subtract(this.point1.p).normalise().normal();
|
||||
// var ac = point.p.subtract(this.point1.p);
|
||||
// return Math.abs(ac.x * n.x + ac.y * n.y);
|
||||
// };
|
||||
|
||||
/**
|
||||
* Renderer handles the layout rendering loop
|
||||
*/
|
||||
const Renderer = Springy.Renderer = function (layout) {
|
||||
this.layout = layout;
|
||||
this.layout.graph.addGraphListener(this);
|
||||
};
|
||||
|
||||
Renderer.prototype.graphChanged = function() {
|
||||
this.start();
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts the simulation of the layout in use.
|
||||
*/
|
||||
Renderer.prototype.start = function(maxTime) {
|
||||
if (maxTime) {
|
||||
setTimeout(() => this.stop(), maxTime);
|
||||
}
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
this.layout.start(res);
|
||||
});
|
||||
};
|
||||
|
||||
Renderer.prototype.stop = function() {
|
||||
this.layout.stop();
|
||||
};
|
||||
|
||||
const isEmpty = function(obj) {
|
||||
for (const k in obj) {
|
||||
if (obj.hasOwnProperty(k)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return Springy;
|
||||
}();
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Generated
+12035
-598
File diff suppressed because it is too large
Load Diff
+24
-22
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.48.8",
|
||||
"version": "0.49.1-beta",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
@@ -26,47 +26,49 @@
|
||||
"dependencies": {
|
||||
"archiver": "5.3.0",
|
||||
"async-mutex": "0.3.2",
|
||||
"axios": "0.23.0",
|
||||
"better-sqlite3": "7.4.3",
|
||||
"body-parser": "1.19.0",
|
||||
"axios": "0.24.0",
|
||||
"better-sqlite3": "7.4.5",
|
||||
"body-parser": "1.19.1",
|
||||
"chokidar": "3.5.2",
|
||||
"cls-hooked": "4.2.2",
|
||||
"commonmark": "0.30.0",
|
||||
"cookie-parser": "1.4.5",
|
||||
"cookie-parser": "1.4.6",
|
||||
"csurf": "1.11.0",
|
||||
"dayjs": "1.10.7",
|
||||
"ejs": "3.1.6",
|
||||
"electron-debug": "3.2.0",
|
||||
"electron-dl": "3.2.1",
|
||||
"electron-dl": "3.3.0",
|
||||
"electron-find": "1.0.7",
|
||||
"electron-window-state": "5.0.3",
|
||||
"express": "4.17.1",
|
||||
"@electron/remote": "2.0.1",
|
||||
"express": "4.17.2",
|
||||
"express-partial-content": "^1.0.2",
|
||||
"express-rate-limit": "5.5.0",
|
||||
"express-rate-limit": "5.5.1",
|
||||
"express-session": "1.17.2",
|
||||
"fs-extra": "10.0.0",
|
||||
"helmet": "4.6.0",
|
||||
"html": "1.0.0",
|
||||
"html2plaintext": "2.1.2",
|
||||
"html2plaintext": "2.1.4",
|
||||
"http-proxy-agent": "5.0.0",
|
||||
"https-proxy-agent": "5.0.0",
|
||||
"image-type": "4.1.0",
|
||||
"ini": "2.0.0",
|
||||
"is-animated": "^2.0.1",
|
||||
"is-svg": "4.3.1",
|
||||
"is-svg": "4.3.2",
|
||||
"jimp": "0.16.1",
|
||||
"joplin-turndown-plugin-gfm": "1.0.12",
|
||||
"jsdom": "18.0.0",
|
||||
"mime-types": "2.1.33",
|
||||
"multer": "1.4.3",
|
||||
"node-abi": "3.2.0",
|
||||
"open": "8.3.0",
|
||||
"jsdom": "19.0.0",
|
||||
"mime-types": "2.1.34",
|
||||
"multer": "1.4.4",
|
||||
"node-abi": "3.5.0",
|
||||
"normalize-strings": "^1.1.1",
|
||||
"open": "8.4.0",
|
||||
"portscanner": "2.2.0",
|
||||
"rand-token": "1.0.1",
|
||||
"request": "^2.88.2",
|
||||
"rimraf": "3.0.2",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"sanitize-html": "2.5.2",
|
||||
"sanitize-html": "2.6.1",
|
||||
"sax": "1.2.4",
|
||||
"semver": "7.3.5",
|
||||
"serve-favicon": "2.5.0",
|
||||
@@ -76,22 +78,22 @@
|
||||
"tmp": "^0.2.1",
|
||||
"turndown": "7.1.1",
|
||||
"unescape": "1.0.1",
|
||||
"ws": "8.2.3",
|
||||
"ws": "8.4.0",
|
||||
"yauzl": "2.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "13.6.3",
|
||||
"electron-builder": "22.13.1",
|
||||
"electron": "16.0.5",
|
||||
"electron-builder": "22.14.5",
|
||||
"electron-packager": "15.4.0",
|
||||
"electron-rebuild": "3.2.3",
|
||||
"electron-rebuild": "3.2.5",
|
||||
"esm": "3.2.25",
|
||||
"jasmine": "3.10.0",
|
||||
"jsdoc": "3.6.7",
|
||||
"lorem-ipsum": "2.0.4",
|
||||
"rcedit": "3.0.1",
|
||||
"webpack": "5.58.2",
|
||||
"webpack-cli": "4.9.0"
|
||||
"webpack": "5.65.0",
|
||||
"webpack-cli": "4.9.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"electron-installer-debian": "3.1.0"
|
||||
|
||||
@@ -42,7 +42,7 @@ class NoteBuilder {
|
||||
}
|
||||
|
||||
child(childNoteBuilder, prefix = "") {
|
||||
new Branch(becca, {
|
||||
new Branch({
|
||||
branchId: id(),
|
||||
noteId: childNoteBuilder.note.noteId,
|
||||
parentNoteId: this.note.noteId,
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("Parser", () => {
|
||||
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||
expect(rootExp.subExpressions[0].constructor.name).toEqual("PropertyComparisonExp");
|
||||
expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp");
|
||||
expect(rootExp.subExpressions[1].subExpressions[0].constructor.name).toEqual("BeccaFlatTextExp");
|
||||
expect(rootExp.subExpressions[1].subExpressions[0].constructor.name).toEqual("NoteFlatTextExp");
|
||||
expect(rootExp.subExpressions[1].subExpressions[0].tokens).toEqual(["hello", "hi"]);
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@ describe("Parser", () => {
|
||||
|
||||
const subs = rootExp.subExpressions[1].subExpressions;
|
||||
|
||||
expect(subs[0].constructor.name).toEqual("BeccaFlatTextExp");
|
||||
expect(subs[0].constructor.name).toEqual("NoteFlatTextExp");
|
||||
expect(subs[0].tokens).toEqual(["hello", "hi"]);
|
||||
|
||||
expect(subs[1].constructor.name).toEqual("NoteContentProtectedFulltextExp");
|
||||
@@ -182,7 +182,7 @@ describe("Parser", () => {
|
||||
expect(firstSub.propertyName).toEqual('isArchived');
|
||||
|
||||
expect(secondSub.constructor.name).toEqual("OrExp");
|
||||
expect(secondSub.subExpressions[0].constructor.name).toEqual("BeccaFlatTextExp");
|
||||
expect(secondSub.subExpressions[0].constructor.name).toEqual("NoteFlatTextExp");
|
||||
expect(secondSub.subExpressions[0].tokens).toEqual(["hello"]);
|
||||
|
||||
expect(thirdSub.constructor.name).toEqual("LabelComparisonExp");
|
||||
|
||||
@@ -13,7 +13,7 @@ describe("Search", () => {
|
||||
becca.reset();
|
||||
|
||||
rootNote = new NoteBuilder(new Note({noteId: 'root', title: 'root', type: 'text'}));
|
||||
new Branch(becca, {branchId: 'root', noteId: 'root', parentNoteId: 'none', notePosition: 10});
|
||||
new Branch({branchId: 'root', noteId: 'root', parentNoteId: 'none', notePosition: 10});
|
||||
});
|
||||
|
||||
it("simple path match", () => {
|
||||
@@ -157,6 +157,21 @@ describe("Search", () => {
|
||||
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("inherited label comparison", () => {
|
||||
rootNote
|
||||
.child(note("Europe")
|
||||
.label('country', '', true)
|
||||
.child(note("Austria"))
|
||||
.child(note("Czech Republic"))
|
||||
);
|
||||
|
||||
const searchContext = new SearchContext();
|
||||
|
||||
const searchResults = searchService.findResultsWithQuery('austria #country', searchContext);
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("numeric label comparison fallback to string comparison", () => {
|
||||
// dates should not be coerced into numbers which would then give wrong numbers
|
||||
|
||||
@@ -169,7 +184,7 @@ describe("Search", () => {
|
||||
.label('established', '1993-01-01'))
|
||||
.child(note("Hungary")
|
||||
.label('established', '1920-06-04'))
|
||||
);
|
||||
);
|
||||
|
||||
const searchContext = new SearchContext();
|
||||
|
||||
@@ -218,7 +233,7 @@ describe("Search", () => {
|
||||
test("#month = month", 1);
|
||||
test("#month = 'MONTH'", 0);
|
||||
|
||||
test("note.dateCreated =* month", 1);
|
||||
test("note.dateCreated =* month", 2);
|
||||
|
||||
test("#date = TODAY", 1);
|
||||
test("#date = today", 1);
|
||||
@@ -337,11 +352,11 @@ describe("Search", () => {
|
||||
|
||||
const searchContext = new SearchContext();
|
||||
|
||||
let searchResults = searchService.findResultsWithQuery('#city AND note.getAncestors().title = Europe', searchContext);
|
||||
let searchResults = searchService.findResultsWithQuery('#city AND note.ancestors.title = Europe', searchContext);
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "Prague")).toBeTruthy();
|
||||
|
||||
searchResults = searchService.findResultsWithQuery('#city AND note.getAncestors().title = Asia', searchContext);
|
||||
searchResults = searchService.findResultsWithQuery('#city AND note.ancestors.title = Asia', searchContext);
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "Taipei")).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ const session = require('express-session');
|
||||
const FileStore = require('session-file-store')(session);
|
||||
const sessionSecret = require('./services/session_secret');
|
||||
const dataDir = require('./services/data_dir');
|
||||
const utils = require('./services/utils');
|
||||
require('./services/handlers');
|
||||
require('./becca/becca_loader.js');
|
||||
|
||||
@@ -101,6 +102,10 @@ require('./services/consistency_checks');
|
||||
|
||||
require('./services/scheduler');
|
||||
|
||||
if (utils.isElectron()) {
|
||||
require('@electron/remote/main').initialize();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
app,
|
||||
sessionParser
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
const sql = require("../services/sql.js");
|
||||
const NoteSet = require("../services/search/note_set");
|
||||
|
||||
/**
|
||||
* Becca is a backend cache of all notes, branches and attributes. There's a similar frontend cache Froca.
|
||||
*/
|
||||
class Becca {
|
||||
constructor() {
|
||||
this.reset();
|
||||
|
||||
@@ -29,15 +29,15 @@ function load() {
|
||||
// using raw query and passing arrays to avoid allocating new objects
|
||||
// this is worth it for becca load since it happens every run and blocks the app until finished
|
||||
|
||||
for (const row of sql.getRawRows(`SELECT noteId, title, type, mime, isProtected, dateCreated, dateModified, utcDateCreated, utcDateModified FROM notes WHERE isDeleted = 0`, [])) {
|
||||
for (const row of sql.getRawRows(`SELECT noteId, title, type, mime, isProtected, dateCreated, dateModified, utcDateCreated, utcDateModified FROM notes WHERE isDeleted = 0`)) {
|
||||
new Note().update(row).init();
|
||||
}
|
||||
|
||||
for (const row of sql.getRawRows(`SELECT branchId, noteId, parentNoteId, prefix, notePosition, isExpanded, utcDateModified FROM branches WHERE isDeleted = 0`, [])) {
|
||||
for (const row of sql.getRawRows(`SELECT branchId, noteId, parentNoteId, prefix, notePosition, isExpanded, utcDateModified FROM branches WHERE isDeleted = 0`)) {
|
||||
new Branch().update(row).init();
|
||||
}
|
||||
|
||||
for (const row of sql.getRawRows(`SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified FROM attributes WHERE isDeleted = 0`, [])) {
|
||||
for (const row of sql.getRawRows(`SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified FROM attributes WHERE isDeleted = 0`)) {
|
||||
new Attribute().update(row).init();
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,10 @@ class Branch extends AbstractEntity {
|
||||
}
|
||||
|
||||
init() {
|
||||
this.becca.branches[this.branchId] = this;
|
||||
if (this.branchId) {
|
||||
this.becca.branches[this.branchId] = this;
|
||||
}
|
||||
|
||||
this.becca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
|
||||
|
||||
if (this.branchId === 'root') {
|
||||
@@ -84,7 +87,7 @@ class Branch extends AbstractEntity {
|
||||
/** @returns {Note} */
|
||||
get childNote() {
|
||||
if (!(this.noteId in this.becca.notes)) {
|
||||
// entities can come out of order in sync, create skeleton which will be filled later
|
||||
// entities can come out of order in sync/import, create skeleton which will be filled later
|
||||
this.becca.addNote(this.noteId, new Note({noteId: this.noteId}));
|
||||
}
|
||||
|
||||
@@ -98,7 +101,7 @@ class Branch extends AbstractEntity {
|
||||
/** @returns {Note} */
|
||||
get parentNote() {
|
||||
if (!(this.parentNoteId in this.becca.notes)) {
|
||||
// entities can come out of order in sync, create skeleton which will be filled later
|
||||
// entities can come out of order in sync/import, create skeleton which will be filled later
|
||||
this.becca.addNote(this.parentNoteId, new Note({noteId: this.parentNoteId}));
|
||||
}
|
||||
|
||||
@@ -136,9 +139,7 @@ class Branch extends AbstractEntity {
|
||||
notePosition: this.notePosition,
|
||||
isExpanded: this.isExpanded,
|
||||
isDeleted: false,
|
||||
utcDateModified: this.utcDateModified,
|
||||
// not used for anything, will be later dropped
|
||||
utcDateCreated: dateUtils.utcNowDateTime()
|
||||
utcDateModified: this.utcDateModified
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,10 @@ class Note extends AbstractEntity {
|
||||
return this.parentBranches;
|
||||
}
|
||||
|
||||
/** @returns {Branch[]} */
|
||||
/**
|
||||
* @returns {Branch[]}
|
||||
* @deprecated use getParentBranches() instead
|
||||
*/
|
||||
getBranches() {
|
||||
return this.parentBranches;
|
||||
}
|
||||
|
||||
@@ -37,9 +37,7 @@ class Option extends AbstractEntity {
|
||||
name: this.name,
|
||||
value: this.value,
|
||||
isSynced: this.isSynced,
|
||||
utcDateModified: this.utcDateModified,
|
||||
// utcDateCreated is scheduled for removal so the value does not matter
|
||||
utcDateCreated: dateUtils.utcNowDateTime()
|
||||
utcDateModified: this.utcDateModified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,9 @@ noteAutocompleteService.init();
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const electron = utils.dynamicRequire('electron');
|
||||
const {webContents} = electron.remote.getCurrentWindow();
|
||||
|
||||
const remote = utils.dynamicRequire('@electron/remote');
|
||||
const {webContents} = remote.getCurrentWindow();
|
||||
|
||||
webContents.on('context-menu', (event, params) => {
|
||||
const {editFlags} = params;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import utils from '../services/utils.js';
|
||||
import treeService from "../services/tree.js";
|
||||
import importService from "../services/import.js";
|
||||
import options from "../services/options.js";
|
||||
|
||||
const $dialog = $("#import-dialog");
|
||||
const $form = $("#import-form");
|
||||
@@ -8,6 +9,7 @@ const $noteTitle = $dialog.find(".import-note-title");
|
||||
const $fileUploadInput = $("#import-file-upload-input");
|
||||
const $importButton = $("#import-button");
|
||||
const $safeImportCheckbox = $("#safe-import-checkbox");
|
||||
const $shrinkImagesWrapper = $("shrink-images-wrapper");
|
||||
const $shrinkImagesCheckbox = $("#shrink-images-checkbox");
|
||||
const $textImportedAsTextCheckbox = $("#text-imported-as-text-checkbox");
|
||||
const $codeImportedAsCodeCheckbox = $("#code-imported-as-code-checkbox");
|
||||
@@ -21,7 +23,7 @@ export async function showDialog(noteId) {
|
||||
$fileUploadInput.val('').trigger('change'); // to trigger Import button disabling listener below
|
||||
|
||||
$safeImportCheckbox.prop("checked", true);
|
||||
$shrinkImagesCheckbox.prop("checked", true);
|
||||
$shrinkImagesCheckbox.prop("checked", options.is('compressImages'));
|
||||
$textImportedAsTextCheckbox.prop("checked", true);
|
||||
$codeImportedAsCodeCheckbox.prop("checked", true);
|
||||
$explodeArchivesCheckbox.prop("checked", true);
|
||||
|
||||
@@ -50,21 +50,21 @@ export default class BackupOptions {
|
||||
|
||||
this.$dailyBackupEnabled.on('change', () => {
|
||||
const opts = { 'dailyBackupEnabled': this.$dailyBackupEnabled.is(":checked") ? "true" : "false" };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$weeklyBackupEnabled.on('change', () => {
|
||||
const opts = { 'weeklyBackupEnabled': this.$weeklyBackupEnabled.is(":checked") ? "true" : "false" };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$monthlyBackupEnabled.on('change', () => {
|
||||
const opts = { 'monthlyBackupEnabled': this.$monthlyBackupEnabled.is(":checked") ? "true" : "false" };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -3,6 +3,13 @@ import server from "../../services/server.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
|
||||
const TPL = `
|
||||
<style>
|
||||
.disabled-field {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<h4>Spell check</h4>
|
||||
|
||||
@@ -27,15 +34,22 @@ const TPL = `
|
||||
|
||||
<div>
|
||||
<h4>Image compression</h4>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="image-max-width-height">Max width / height of an image in pixels (image will be resized if it exceeds this setting).</label>
|
||||
<input class="form-control" id="image-max-width-height" type="number" min="1">
|
||||
<input id="image-compresion-enabled" type="checkbox" name="image-compression-enabled">
|
||||
<label for="image-compresion-enabled">Enable image compression</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="image-jpeg-quality">JPEG quality (0 - worst quality, 100 best quality, 50 - 85 is recommended)</label>
|
||||
<input class="form-control" id="image-jpeg-quality" min="0" max="100" type="number">
|
||||
<div id="image-compression-enabled-wraper">
|
||||
<div class="form-group">
|
||||
<label for="image-max-width-height">Max width / height of an image in pixels (image will be resized if it exceeds this setting).</label>
|
||||
<input class="form-control" id="image-max-width-height" type="number" min="1">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="image-jpeg-quality">JPEG quality (10 - worst quality, 100 best quality, 50 - 85 is recommended)</label>
|
||||
<input class="form-control" id="image-jpeg-quality" min="10" max="100" type="number">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -107,14 +121,14 @@ export default class ProtectedSessionOptions {
|
||||
|
||||
this.$spellCheckEnabled.on('change', () => {
|
||||
const opts = { 'spellCheckEnabled': this.$spellCheckEnabled.is(":checked") ? "true" : "false" };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$spellCheckLanguageCode.on('change', () => {
|
||||
const opts = { 'spellCheckLanguageCode': this.$spellCheckLanguageCode.val() };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
@@ -122,7 +136,7 @@ export default class ProtectedSessionOptions {
|
||||
this.$availableLanguageCodes = $("#available-language-codes");
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const {webContents} = utils.dynamicRequire('electron').remote.getCurrentWindow();
|
||||
const {webContents} = utils.dynamicRequire('@electron/remote').getCurrentWindow();
|
||||
|
||||
this.$availableLanguageCodes.text(webContents.session.availableSpellCheckerLanguages.join(', '));
|
||||
}
|
||||
@@ -133,7 +147,7 @@ export default class ProtectedSessionOptions {
|
||||
const eraseEntitiesAfterTimeInSeconds = this.$eraseEntitiesAfterTimeInSeconds.val();
|
||||
|
||||
server.put('options', { 'eraseEntitiesAfterTimeInSeconds': eraseEntitiesAfterTimeInSeconds }).then(() => {
|
||||
toastService.showMessage("Options change have been saved.");
|
||||
toastService.showMessage("Options changed have been saved.");
|
||||
});
|
||||
|
||||
return false;
|
||||
@@ -152,7 +166,7 @@ export default class ProtectedSessionOptions {
|
||||
const protectedSessionTimeout = this.$protectedSessionTimeout.val();
|
||||
|
||||
server.put('options', { 'protectedSessionTimeout': protectedSessionTimeout }).then(() => {
|
||||
toastService.showMessage("Options change have been saved.");
|
||||
toastService.showMessage("Options changed have been saved.");
|
||||
});
|
||||
|
||||
return false;
|
||||
@@ -162,7 +176,7 @@ export default class ProtectedSessionOptions {
|
||||
|
||||
this.$noteRevisionsTimeInterval.on('change', () => {
|
||||
const opts = { 'noteRevisionSnapshotTimeInterval': this.$noteRevisionsTimeInterval.val() };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
@@ -172,14 +186,14 @@ export default class ProtectedSessionOptions {
|
||||
|
||||
this.$imageMaxWidthHeight.on('change', () => {
|
||||
const opts = { 'imageMaxWidthHeight': this.$imageMaxWidthHeight.val() };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$imageJpegQuality.on('change', () => {
|
||||
const opts = { 'imageJpegQuality': this.$imageJpegQuality.val() };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
@@ -188,7 +202,7 @@ export default class ProtectedSessionOptions {
|
||||
|
||||
this.$autoReadonlySizeText.on('change', () => {
|
||||
const opts = { 'autoReadonlySizeText': this.$autoReadonlySizeText.val() };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
@@ -197,10 +211,30 @@ export default class ProtectedSessionOptions {
|
||||
|
||||
this.$autoReadonlySizeCode.on('change', () => {
|
||||
const opts = { 'autoReadonlySizeCode': this.$autoReadonlySizeText.val() };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$enableImageCompression = $("#image-compresion-enabled");
|
||||
this.$imageCompressionWrapper = $("#image-compression-enabled-wraper");
|
||||
|
||||
this.setImageCompression = (isChecked) => {
|
||||
if (isChecked) {
|
||||
this.$imageCompressionWrapper.removeClass("disabled-field");
|
||||
} else {
|
||||
this.$imageCompressionWrapper.addClass("disabled-field");
|
||||
}
|
||||
}
|
||||
|
||||
this.$enableImageCompression.on("change", () => {
|
||||
const isChecked = this.$enableImageCompression.prop("checked");
|
||||
const opts = { 'compressImages': isChecked ? 'true' : 'false' };
|
||||
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
this.setImageCompression(isChecked);
|
||||
})
|
||||
}
|
||||
|
||||
optionsLoaded(options) {
|
||||
@@ -216,5 +250,9 @@ export default class ProtectedSessionOptions {
|
||||
|
||||
this.$autoReadonlySizeText.val(options['autoReadonlySizeText']);
|
||||
this.$autoReadonlySizeCode.val(options['autoReadonlySizeCode']);
|
||||
|
||||
const compressImages = options['compressImages'] === 'true';
|
||||
this.$enableImageCompression.prop('checked', compressImages);
|
||||
this.setImageCompression(compressImages);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export default class SyncOptions {
|
||||
'syncProxy': this.$syncProxy.val()
|
||||
};
|
||||
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -130,18 +130,38 @@ class NoteShort {
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {string[]} */
|
||||
getBranchIds() {
|
||||
/**
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getParentBranchIds() {
|
||||
return Object.values(this.parentToBranch);
|
||||
}
|
||||
|
||||
/** @returns {Branch[]} */
|
||||
getBranches() {
|
||||
/**
|
||||
* @returns {string[]}
|
||||
* @deprecated use getParentBranchIds() instead
|
||||
*/
|
||||
getBranchIds() {
|
||||
return this.getParentBranchIds();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Branch[]}
|
||||
*/
|
||||
getParentBranches() {
|
||||
const branchIds = Object.values(this.parentToBranch);
|
||||
|
||||
return this.froca.getBranches(branchIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Branch[]}
|
||||
* @deprecated use getParentBranches() instead
|
||||
*/
|
||||
getBranches() {
|
||||
return this.getParentBranches();
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
hasChildren() {
|
||||
return this.children.length > 0;
|
||||
@@ -378,6 +398,9 @@ class NoteShort {
|
||||
else if (this.noteId === 'root') {
|
||||
return "bx bx-chevrons-right";
|
||||
}
|
||||
if (this.noteId === 'share') {
|
||||
return "bx bx-share-alt";
|
||||
}
|
||||
else if (this.type === 'text') {
|
||||
if (this.isFolder()) {
|
||||
return "bx bx-folder";
|
||||
@@ -620,8 +643,8 @@ class NoteShort {
|
||||
});
|
||||
}
|
||||
|
||||
hasAncestor(ancestorNote, visitedNoteIds = null) {
|
||||
if (this.noteId === ancestorNote.noteId) {
|
||||
hasAncestor(ancestorNoteId, visitedNoteIds = null) {
|
||||
if (this.noteId === ancestorNoteId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -635,13 +658,13 @@ class NoteShort {
|
||||
visitedNoteIds.add(this.noteId);
|
||||
|
||||
for (const templateNote of this.getTemplateNotes()) {
|
||||
if (templateNote.hasAncestor(ancestorNote, visitedNoteIds)) {
|
||||
if (templateNote.hasAncestor(ancestorNoteId, visitedNoteIds)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const parentNote of this.getParentNotes()) {
|
||||
if (parentNote.hasAncestor(ancestorNote, visitedNoteIds)) {
|
||||
if (parentNote.hasAncestor(ancestorNoteId, visitedNoteIds)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -758,6 +781,26 @@ class NoteShort {
|
||||
throw new Error(`Unrecognized env type ${env} for note ${this.noteId}`);
|
||||
}
|
||||
}
|
||||
|
||||
isShared() {
|
||||
for (const parentNoteId of this.parents) {
|
||||
if (parentNoteId === 'root' || parentNoteId === 'none') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parentNote = froca.notes[parentNoteId];
|
||||
|
||||
if (!parentNote) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parentNote.noteId === 'share' || parentNote.isShared()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default NoteShort;
|
||||
|
||||
@@ -45,6 +45,9 @@ import EditedNotesWidget from "../widgets/ribbon_widgets/edited_notes.js";
|
||||
import OpenNoteButtonWidget from "../widgets/buttons/open_note_button_widget.js";
|
||||
import MermaidWidget from "../widgets/mermaid.js";
|
||||
import BookmarkButtons from "../widgets/bookmark_buttons.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import BacklinksWidget from "../widgets/backlinks.js";
|
||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
|
||||
export default class DesktopLayout {
|
||||
constructor(customWidgets) {
|
||||
@@ -110,9 +113,7 @@ export default class DesktopLayout {
|
||||
.collapsible()
|
||||
.id('center-pane')
|
||||
.child(new SplitNoteContainer(() =>
|
||||
new FlexContainer('column')
|
||||
.css("flex-grow", "1")
|
||||
.collapsible()
|
||||
new NoteWrapperWidget()
|
||||
.child(new FlexContainer('row').class('title-row')
|
||||
.css("height", "50px")
|
||||
.css('align-items', "center")
|
||||
@@ -147,7 +148,9 @@ export default class DesktopLayout {
|
||||
.titlePlacement("bottom"))
|
||||
.button(new NoteActionsWidget())
|
||||
)
|
||||
.child(new SharedInfoWidget())
|
||||
.child(new NoteUpdateStatusWidget())
|
||||
.child(new BacklinksWidget())
|
||||
.child(new MermaidWidget())
|
||||
.child(
|
||||
new ScrollingContainer()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import NoteTitleWidget from "../widgets/note_title.js";
|
||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import MobileGlobalButtonsWidget from "../widgets/mobile_widgets/mobile_global_buttons.js";
|
||||
import CloseDetailButtonWidget from "../widgets/mobile_widgets/close_detail_button.js";
|
||||
@@ -27,13 +28,19 @@ kbd {
|
||||
padding-right: 0.5em;
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
.quick-search {
|
||||
margin: 55px 0px 0px 0px;
|
||||
}
|
||||
.quick-search .dropdown-menu {
|
||||
max-width: 350px;
|
||||
}
|
||||
</style>`;
|
||||
|
||||
const FANCYTREE_CSS = `
|
||||
<style>
|
||||
.tree-wrapper {
|
||||
max-height: 100%;
|
||||
margin-top: 55px;
|
||||
margin-top: 0px;
|
||||
overflow-y: auto;
|
||||
contain: content;
|
||||
padding-left: 10px;
|
||||
@@ -95,6 +102,7 @@ export default class MobileLayout {
|
||||
.css('padding-left', 0)
|
||||
.css('contain', 'content')
|
||||
.child(new MobileGlobalButtonsWidget())
|
||||
.child(new QuickSearchWidget())
|
||||
.child(new NoteTreeWidget("main")
|
||||
.cssBlock(FANCYTREE_CSS)))
|
||||
.child(new ScreenContainer("detail", "column")
|
||||
|
||||
@@ -50,7 +50,7 @@ function isAffecting(attrRow, affectedNote) {
|
||||
|
||||
if (this.isInheritable) {
|
||||
for (const owningNote of owningNotes) {
|
||||
if (owningNote.hasAncestor(attrNote)) {
|
||||
if (owningNote.hasAncestor(attrNote.noteId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import branchService from "./branches.js";
|
||||
import toastService from "./toast.js";
|
||||
import hoistedNoteService from "./hoisted_note.js";
|
||||
import froca from "./froca.js";
|
||||
import linkService from "./link.js";
|
||||
import utils from "./utils.js";
|
||||
|
||||
let clipboardBranchIds = [];
|
||||
let clipboardMode = null;
|
||||
@@ -60,10 +61,23 @@ async function pasteInto(parentBranchId) {
|
||||
}
|
||||
}
|
||||
|
||||
function copy(branchIds) {
|
||||
async function copy(branchIds) {
|
||||
clipboardBranchIds = branchIds;
|
||||
clipboardMode = 'copy';
|
||||
|
||||
if (utils.isElectron()) {
|
||||
// https://github.com/zadam/trilium/issues/2401
|
||||
const {clipboard} = require('electron');
|
||||
const links = [];
|
||||
|
||||
for (const branch of froca.getBranches(clipboardBranchIds)) {
|
||||
const $link = await linkService.createNoteLink(branch.parentNoteId + '/' + branch.noteId, { referenceLink: true });
|
||||
links.push($link[0].outerHTML);
|
||||
}
|
||||
|
||||
clipboard.writeHTML(links.join(', '));
|
||||
}
|
||||
|
||||
toastService.showMessage("Note(s) have been copied into clipboard.");
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export default class Entrypoints extends Component {
|
||||
|
||||
openDevToolsCommand() {
|
||||
if (utils.isElectron()) {
|
||||
utils.dynamicRequire('electron').remote.getCurrentWindow().toggleDevTools();
|
||||
utils.dynamicRequire('@electron/remote').getCurrentWindow().toggleDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export default class Entrypoints extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
const {remote} = utils.dynamicRequire('electron');
|
||||
const remote = utils.dynamicRequire('@electron/remote');
|
||||
const {FindInPage} = utils.dynamicRequire('electron-find');
|
||||
const findInPage = new FindInPage(remote.getCurrentWebContents(), {
|
||||
offsetTop: 10,
|
||||
@@ -116,7 +116,7 @@ export default class Entrypoints extends Component {
|
||||
|
||||
toggleFullscreenCommand() {
|
||||
if (utils.isElectron()) {
|
||||
const win = utils.dynamicRequire('electron').remote.getCurrentWindow();
|
||||
const win = utils.dynamicRequire('@electron/remote').getCurrentWindow();
|
||||
|
||||
if (win.isFullScreenable()) {
|
||||
win.setFullScreen(!win.isFullScreen());
|
||||
@@ -143,7 +143,7 @@ export default class Entrypoints extends Component {
|
||||
backInNoteHistoryCommand() {
|
||||
if (utils.isElectron()) {
|
||||
// standard JS version does not work completely correctly in electron
|
||||
const webContents = utils.dynamicRequire('electron').remote.getCurrentWebContents();
|
||||
const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
|
||||
const activeIndex = parseInt(webContents.getActiveIndex());
|
||||
|
||||
webContents.goToIndex(activeIndex - 1);
|
||||
@@ -156,7 +156,7 @@ export default class Entrypoints extends Component {
|
||||
forwardInNoteHistoryCommand() {
|
||||
if (utils.isElectron()) {
|
||||
// standard JS version does not work completely correctly in electron
|
||||
const webContents = utils.dynamicRequire('electron').remote.getCurrentWebContents();
|
||||
const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
|
||||
const activeIndex = parseInt(webContents.getActiveIndex());
|
||||
|
||||
webContents.goToIndex(activeIndex + 1);
|
||||
@@ -213,9 +213,13 @@ export default class Entrypoints extends Component {
|
||||
} else if (note.mime.endsWith("env=backend")) {
|
||||
await server.post('script/run/' + note.noteId);
|
||||
} else if (note.mime === 'text/x-sqlite;schema=trilium') {
|
||||
const {results} = await server.post("sql/execute/" + note.noteId);
|
||||
const resp = await server.post("sql/execute/" + note.noteId);
|
||||
|
||||
await appContext.triggerEvent('sqlQueryResults', {ntxId: ntxId, results: results});
|
||||
if (!resp.success) {
|
||||
alert("Error occurred while executing SQL query: " + resp.message);
|
||||
}
|
||||
|
||||
await appContext.triggerEvent('sqlQueryResults', {ntxId: ntxId, results: resp.results});
|
||||
}
|
||||
|
||||
toastService.showMessage("Note executed");
|
||||
|
||||
@@ -6,12 +6,14 @@ import appContext from "./app_context.js";
|
||||
import NoteComplement from "../entities/note_complement.js";
|
||||
|
||||
/**
|
||||
* Froca keeps a read only cache of note tree structure in frontend's memory.
|
||||
* Froca (FROntend CAche) keeps a read only cache of note tree structure in frontend's memory.
|
||||
* - notes are loaded lazily when unknown noteId is requested
|
||||
* - when note is loaded, all its parent and child branches are loaded as well. For a branch to be used, it's not must be loaded before
|
||||
* - deleted notes are present in the cache as well, but they don't have any branches. As a result check for deleted branch is done by presence check - if the branch is not there even though the corresponding note has been loaded, we can infer it is deleted.
|
||||
*
|
||||
* Note and branch deletions are corner cases and usually not needed.
|
||||
*
|
||||
* Backend has a similar cache called Becca
|
||||
*/
|
||||
class Froca {
|
||||
constructor() {
|
||||
@@ -186,7 +188,7 @@ class Froca {
|
||||
froca.notes[note.noteId].childToBranch = {};
|
||||
}
|
||||
|
||||
const branches = [...note.getBranches(), ...note.getChildBranches()];
|
||||
const branches = [...note.getParentBranches(), ...note.getChildBranches()];
|
||||
|
||||
searchResultNoteIds.forEach((resultNoteId, index) => branches.push({
|
||||
// branchId should be repeatable since sometimes we reload some notes without rerendering the tree
|
||||
@@ -259,6 +261,7 @@ class Froca {
|
||||
return (await this.getNotes([noteId], silentNotFoundError))[0];
|
||||
}
|
||||
|
||||
/** @returns {Note|null} */
|
||||
getNoteFromCache(noteId) {
|
||||
if (!noteId) {
|
||||
throw new Error("Empty noteId");
|
||||
@@ -267,6 +270,7 @@ class Froca {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
|
||||
/** @returns {Branch[]} */
|
||||
getBranches(branchIds, silentNotFoundError = false) {
|
||||
return branchIds
|
||||
.map(branchId => this.getBranch(branchId, silentNotFoundError))
|
||||
|
||||
@@ -313,6 +313,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
|
||||
* @param {object} [params]
|
||||
* @param {boolean} [params.showTooltip=true] - enable/disable tooltip on the link
|
||||
* @param {boolean} [params.showNotePath=false] - show also whole note's path as part of the link
|
||||
* @param {boolean} [params.showNoteIcon=false] - show also note icon before the title
|
||||
* @param {string} [title=] - custom link tile with note's title as default
|
||||
*/
|
||||
this.createNoteLink = linkService.createNoteLink;
|
||||
|
||||
@@ -35,7 +35,7 @@ async function checkNoteAccess(notePath, noteContext) {
|
||||
|
||||
const hoistedNoteId = noteContext.hoistedNoteId;
|
||||
|
||||
if (!resolvedNotePath.includes(hoistedNoteId)) {
|
||||
if (!resolvedNotePath.includes(hoistedNoteId) && !resolvedNotePath.includes("hidden")) {
|
||||
const confirmDialog = await import('../dialogs/confirm.js');
|
||||
|
||||
if (!await confirmDialog.confirm("Requested note is outside of hoisted note subtree and you must unhoist to access the note. Do you want to proceed with unhoisting?")) {
|
||||
|
||||
@@ -21,6 +21,7 @@ async function createNoteLink(notePath, options = {}) {
|
||||
const showTooltip = options.showTooltip === undefined ? true : options.showTooltip;
|
||||
const showNotePath = options.showNotePath === undefined ? false : options.showNotePath;
|
||||
const showNoteIcon = options.showNoteIcon === undefined ? false : options.showNoteIcon;
|
||||
const referenceLink = options.referenceLink === undefined ? false : options.referenceLink;
|
||||
|
||||
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath);
|
||||
|
||||
@@ -48,6 +49,10 @@ async function createNoteLink(notePath, options = {}) {
|
||||
$noteLink.addClass("no-tooltip-preview");
|
||||
}
|
||||
|
||||
if (referenceLink) {
|
||||
$noteLink.addClass("reference-link");
|
||||
}
|
||||
|
||||
$container.append($noteLink);
|
||||
|
||||
if (showNotePath) {
|
||||
|
||||
@@ -7,7 +7,7 @@ function getFileUrl(noteId) {
|
||||
|
||||
function download(url) {
|
||||
if (utils.isElectron()) {
|
||||
const remote = utils.dynamicRequire('electron').remote;
|
||||
const remote = utils.dynamicRequire('@electron/remote');
|
||||
|
||||
remote.getCurrentWebContents().downloadURL(url);
|
||||
} else {
|
||||
|
||||
@@ -129,12 +129,12 @@ export default class TabManager extends Component {
|
||||
window.history.pushState(null, "", url);
|
||||
}
|
||||
|
||||
document.title = "Trilium Notes";
|
||||
|
||||
if (activeNoteContext.note) {
|
||||
const titleFragments = [
|
||||
// it helps navigating in history if note title is included in the title
|
||||
document.title += " - " + activeNoteContext.note.title;
|
||||
}
|
||||
activeNoteContext.note?.title,
|
||||
"Trilium Notes"
|
||||
].filter(Boolean);
|
||||
document.title = titleFragments.join(" - ");
|
||||
|
||||
this.triggerEvent('activeNoteChanged'); // trigger this even in on popstate event
|
||||
}
|
||||
@@ -237,7 +237,7 @@ export default class TabManager extends Component {
|
||||
if (noteContext) {
|
||||
const resolvedNotePath = await treeService.resolveNotePath(notePath, noteContext.hoistedNoteId);
|
||||
|
||||
if (resolvedNotePath.includes(noteContext.hoistedNoteId)) {
|
||||
if (resolvedNotePath.includes(noteContext.hoistedNoteId) || resolvedNotePath.includes("hidden")) {
|
||||
hoistedNoteId = noteContext.hoistedNoteId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,9 @@ async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logEr
|
||||
if (logErrors) {
|
||||
const parent = froca.getNoteFromCache(parentNoteId);
|
||||
|
||||
console.debug(utils.now(), `Did not find parent ${parentNoteId} (${parent ? parent.title : 'n/a'}) for child ${childNoteId} (${child.title}), available parents: ${parents.map(p => `${p.noteId} (${p.title})`)}. You can ignore this message as it is mostly harmless.`);
|
||||
console.debug(utils.now(), `Did not find parent ${parentNoteId} (${parent ? parent.title : 'n/a'})
|
||||
for child ${childNoteId} (${child.title}), available parents: ${parents.map(p => `${p.noteId} (${p.title})`)}.
|
||||
You can ignore this message as it is mostly harmless.`);
|
||||
}
|
||||
|
||||
const someNotePath = getSomeNotePath(child, hoistedNoteId);
|
||||
@@ -83,6 +85,10 @@ async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logEr
|
||||
if (someNotePath) { // in case it's root the path may be empty
|
||||
const pathToRoot = someNotePath.split("/").reverse().slice(1);
|
||||
|
||||
if (!pathToRoot.includes("root")) {
|
||||
pathToRoot.push('root');
|
||||
}
|
||||
|
||||
for (const noteId of pathToRoot) {
|
||||
effectivePathSegments.push(noteId);
|
||||
}
|
||||
@@ -138,7 +144,7 @@ ws.subscribeToMessages(message => {
|
||||
appContext.tabManager.activateOrOpenNote(message.noteId);
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const currentWindow = utils.dynamicRequire("electron").remote.getCurrentWindow();
|
||||
const currentWindow = utils.dynamicRequire('@electron/remote').getCurrentWindow();
|
||||
|
||||
currentWindow.show();
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ function isHtmlEmpty(html) {
|
||||
|
||||
async function clearBrowserCache() {
|
||||
if (isElectron()) {
|
||||
const win = dynamicRequire('electron').remote.getCurrentWindow();
|
||||
const win = utils.dynamicRequire('@electron/remote').getCurrentWindow();
|
||||
await win.webContents.session.clearCache();
|
||||
}
|
||||
}
|
||||
@@ -340,10 +340,12 @@ function initHelpDropdown($el) {
|
||||
|
||||
const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/"
|
||||
|
||||
function openHelp(e) {
|
||||
window.open(wikiBaseUrl + $(e.target).attr("data-help-page"), '_blank');
|
||||
}
|
||||
|
||||
function initHelpButtons($el) {
|
||||
$el.on("click", "*[data-help-page]", e => {
|
||||
window.open(wikiBaseUrl + $(e.target).attr("data-help-page"), '_blank');
|
||||
});
|
||||
$el.on("click", "*[data-help-page]", e => openHelp(e));
|
||||
}
|
||||
|
||||
function filterAttributeName(name) {
|
||||
@@ -397,6 +399,7 @@ export default {
|
||||
timeLimit,
|
||||
initHelpDropdown,
|
||||
initHelpButtons,
|
||||
openHelp,
|
||||
filterAttributeName,
|
||||
isValidAttributeName
|
||||
};
|
||||
|
||||
@@ -122,7 +122,7 @@ async function checkOutstandingSyncs() {
|
||||
|
||||
if (initialized) {
|
||||
if (utils.isElectron()) {
|
||||
const remote = utils.dynamicRequire('electron').remote;
|
||||
const remote = utils.dynamicRequire('@electron/remote');
|
||||
remote.app.relaunch();
|
||||
remote.app.exit(0);
|
||||
}
|
||||
|
||||
@@ -189,6 +189,7 @@ const ATTR_HELP = {
|
||||
"runAtHour": "On which hour should this run. Should be used together with <code>#run=hourly</code>. Can be defined multiple times for more runs during the day.",
|
||||
"disableInclusion": "scripts with this label won't be included into parent script execution.",
|
||||
"sorted": "keeps child notes sorted by title alphabetically",
|
||||
"top": "keep given note on top in its parent (applies only on sorted parents)",
|
||||
"hidePromotedAttributes": "Hide promoted attributes on this note",
|
||||
"readOnly": "editor is in read only mode. Works only for text and code notes.",
|
||||
"autoReadOnlyDisabled": "text/code notes can be set automatically into read mode when they are too large. You can disable this behavior on per-note basis by adding this label to the note",
|
||||
@@ -208,6 +209,11 @@ const ATTR_HELP = {
|
||||
"inbox": "default inbox location for new notes",
|
||||
"hoistedInbox": "default inbox location for new notes when hoisted to some ancestor of this note",
|
||||
"sqlConsoleHome": "default location of SQL console notes",
|
||||
"bookmarked": "note with this label will appear in bookmarks",
|
||||
"bookmarkFolder": "note with this label will appear in bookmarks as folder (allowing access to its children)",
|
||||
"shareHiddenFromTree": "this note is hidden from left navigation tree, but still accessible with its URL",
|
||||
"shareAlias": "define an alias using which the note will be available under https://your_trilium_host/share/[your_alias]",
|
||||
"shareOmitDefaultCss": "default share page CSS will be omitted. Use when you make extensive styling changes.",
|
||||
},
|
||||
"relation": {
|
||||
"runOnNoteCreation": "executes when note is created on backend",
|
||||
@@ -218,7 +224,8 @@ const ATTR_HELP = {
|
||||
"runOnAttributeChange": "executes when attribute is changed under this note",
|
||||
"template": "attached note's attributes will be inherited even without parent-child relationship. See template for details.",
|
||||
"renderNote": 'notes of type "render HTML note" will be rendered using a code note (HTML or script) and it is necessary to point using this relation to which note should be rendered',
|
||||
"widget": "target of this relation will be executed and rendered as a widget in the sidebar"
|
||||
"widget": "target of this relation will be executed and rendered as a widget in the sidebar",
|
||||
"shareCss": "CSS note which will be injected into the share page. CSS note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree' and 'shareOmitDefaultCss' as well.",
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import linkService from "../services/link.js";
|
||||
import server from "../services/server.js";
|
||||
import froca from "../services/froca.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="backlinks-widget">
|
||||
<style>
|
||||
.backlinks-widget {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.backlinks-ticker {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 130px;
|
||||
border-radius: 10px;
|
||||
border-color: var(--main-border-color);
|
||||
background-color: var(--more-accented-background-color);
|
||||
padding: 4px 10px 4px 10px;
|
||||
opacity: 70%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.backlinks-count {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.backlinks-close-ticker {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.backlinks-items {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 10px;
|
||||
width: 400px;
|
||||
border-radius: 10px;
|
||||
background-color: #eeeeee;
|
||||
color: #444;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.backlink-excerpt {
|
||||
border-left: 2px solid var(--main-border-color);
|
||||
padding-left: 10px;
|
||||
opacity: 80%;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.backlink-excerpt .backlink-link { /* the actual backlink */
|
||||
font-weight: bold;
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
/* relation map has already buttons in that position */
|
||||
.type-relation-map .backlinks-ticker { top: 50px; }
|
||||
.type-relation-map .backlinks-items { top: 100px; }
|
||||
</style>
|
||||
|
||||
<div class="backlinks-ticker">
|
||||
<span class="backlinks-count"></span>
|
||||
|
||||
<span class="bx bx-x backlinks-close-ticker"></span>
|
||||
</div>
|
||||
|
||||
<div class="backlinks-items" style="display: none;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class BacklinksWidget extends NoteContextAwareWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$count = this.$widget.find('.backlinks-count');
|
||||
this.$items = this.$widget.find('.backlinks-items');
|
||||
this.$ticker = this.$widget.find('.backlinks-ticker');
|
||||
|
||||
this.$count.on("click", () => {
|
||||
this.$items.toggle();
|
||||
this.$items.css("max-height", $(window).height() - this.$items.offset().top - 10);
|
||||
|
||||
if (this.$items.is(":visible")) {
|
||||
this.renderBacklinks();
|
||||
}
|
||||
});
|
||||
|
||||
this.$closeTickerButton = this.$widget.find('.backlinks-close-ticker');
|
||||
this.$closeTickerButton.on("click", () => {
|
||||
this.$ticker.hide();
|
||||
|
||||
this.clearItems();
|
||||
});
|
||||
|
||||
this.contentSized();
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
this.clearItems();
|
||||
|
||||
// can't use froca since that would count only relations from loaded notes
|
||||
const resp = await server.get(`notes/${this.noteId}/backlink-count`);
|
||||
|
||||
if (!resp || !resp.count) {
|
||||
this.$ticker.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
this.$ticker.show();
|
||||
this.$count.text(
|
||||
`${resp.count} backlink`
|
||||
+ (resp.count === 1 ? '' : 's')
|
||||
);
|
||||
}
|
||||
|
||||
clearItems() {
|
||||
this.$items.empty().hide();
|
||||
}
|
||||
|
||||
async renderBacklinks() {
|
||||
if (!this.note) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$items.empty();
|
||||
|
||||
const backlinks = await server.get(`note-map/${this.noteId}/backlinks`);
|
||||
|
||||
if (!backlinks.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
await froca.getNotes(backlinks.map(bl => bl.noteId)); // prefetch all
|
||||
|
||||
for (const backlink of backlinks) {
|
||||
const $item = $("<div>");
|
||||
|
||||
$item.append(await linkService.createNoteLink(backlink.noteId, {
|
||||
showNoteIcon: true,
|
||||
showNotePath: true,
|
||||
showTooltip: false
|
||||
}));
|
||||
|
||||
if (backlink.relationName) {
|
||||
$item.append($("<p>").text("relation: " + backlink.relationName));
|
||||
}
|
||||
else {
|
||||
$item.append(...backlink.excerpts);
|
||||
}
|
||||
|
||||
this.$items.append($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +1,32 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import protectedSessionService from "../services/protected_session.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import SwitchWidget from "./switch.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="bookmark-switch">
|
||||
<style>
|
||||
/* The switch - the box around the slider */
|
||||
.bookmark-switch .switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.bookmark-switch .slider {
|
||||
border-radius: 24px;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--more-accented-background-color);
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.bookmark-switch .slider:before {
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: var(--main-background-color);
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.bookmark-switch .slider.checked {
|
||||
background-color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.bookmark-switch .slider.checked:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="add-bookmark-button">
|
||||
Bookmark
|
||||
|
||||
|
||||
|
||||
<span title="Bookmark this note to the left side panel">
|
||||
<label class="switch">
|
||||
<span class="slider"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="remove-bookmark-button">
|
||||
Bookmark
|
||||
|
||||
|
||||
|
||||
<span title="Remove bookmark">
|
||||
<label class="switch">
|
||||
<span class="slider checked"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class BookmarkSwitchWidget extends NoteContextAwareWidget {
|
||||
export default class BookmarkSwitchWidget extends SwitchWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
super.doRender();
|
||||
|
||||
this.$addBookmarkButton = this.$widget.find(".add-bookmark-button");
|
||||
this.$addBookmarkButton.on('click', () => attributeService.setLabel(this.noteId, 'bookmarked'));
|
||||
this.$switchOnName.text("Bookmark");
|
||||
this.$switchOnButton.attr("title", "Bookmark this note to the left side panel");
|
||||
|
||||
this.$removeBookmarkButton = this.$widget.find(".remove-bookmark-button");
|
||||
this.$removeBookmarkButton.on('click', async () => {
|
||||
for (const label of this.note.getLabels('bookmarked')) {
|
||||
await attributeService.removeAttributeById(this.noteId, label.attributeId);
|
||||
}
|
||||
});
|
||||
this.$switchOffName.text("Bookmark");
|
||||
this.$switchOffButton.attr("title", "Remove bookmark");
|
||||
}
|
||||
|
||||
async switchOff() {
|
||||
for (const label of this.note.getLabels('bookmarked')) {
|
||||
await attributeService.removeAttributeById(this.noteId, label.attributeId);
|
||||
}
|
||||
}
|
||||
|
||||
switchOn() {
|
||||
return attributeService.setLabel(this.noteId, 'bookmarked');
|
||||
}
|
||||
|
||||
refreshWithNote(note) {
|
||||
const isBookmarked = note.hasLabel('bookmarked');
|
||||
|
||||
this.$addBookmarkButton.toggle(!isBookmarked);
|
||||
this.$removeBookmarkButton.toggle(isBookmarked);
|
||||
this.$switchOn.toggle(!isBookmarked);
|
||||
this.$switchOff.toggle(isBookmarked);
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
|
||||
@@ -44,7 +44,10 @@ export default class ButtonWidget extends NoteContextAwareWidget {
|
||||
this.$widget.tooltip({
|
||||
html: true,
|
||||
title: () => {
|
||||
const title = this.settings.title;
|
||||
const title = typeof this.settings.title === "function"
|
||||
? this.settings.title()
|
||||
: this.settings.title;
|
||||
|
||||
const action = actions.find(act => act.actionName === this.settings.command);
|
||||
|
||||
if (action && action.effectiveShortcuts.length > 0) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import UpdateAvailableWidget from "./update_available.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="dropdown global-menu dropright">
|
||||
@@ -10,7 +11,7 @@ const TPL = `
|
||||
}
|
||||
|
||||
.global-menu .dropdown-menu {
|
||||
width: 20em;
|
||||
min-width: 20em;
|
||||
}
|
||||
|
||||
.global-menu-button {
|
||||
@@ -19,16 +20,30 @@ const TPL = `
|
||||
background-position: 50% 45%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.global-menu-button:hover {
|
||||
background-image: url("images/icon-color.png");
|
||||
}
|
||||
|
||||
.global-menu-button-update-available {
|
||||
position: absolute;
|
||||
right: -30px;
|
||||
bottom: -30px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button type="button" data-toggle="dropdown" data-placement="right"
|
||||
aria-haspopup="true" aria-expanded="false"
|
||||
class="icon-action global-menu-button" title="Menu"></button>
|
||||
class="icon-action global-menu-button" title="Menu">
|
||||
<div class="global-menu-button-update-available"></div>
|
||||
</button>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item options-button" data-trigger-command="showOptions">
|
||||
@@ -89,6 +104,12 @@ const TPL = `
|
||||
About Trilium Notes
|
||||
</a>
|
||||
|
||||
<a class="dropdown-item update-to-latest-version-button" data-trigger-command="downloadLatestVersion">
|
||||
<span class="bx bx-sync"></span>
|
||||
|
||||
<span class="version-text"></span>
|
||||
</a>
|
||||
|
||||
<a class="dropdown-item logout-button" data-trigger-command="logout">
|
||||
<span class="bx bx-log-out"></span>
|
||||
Logout
|
||||
@@ -98,6 +119,12 @@ const TPL = `
|
||||
`;
|
||||
|
||||
export default class GlobalMenuWidget extends BasicWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.updateAvailableWidget = new UpdateAvailableWidget();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
@@ -114,7 +141,40 @@ export default class GlobalMenuWidget extends BasicWidget {
|
||||
this.$widget.find(".open-dev-tools-button").toggle(isElectron);
|
||||
this.$widget.find(".switch-to-mobile-version-button").toggle(!isElectron);
|
||||
|
||||
|
||||
this.$widget.on('click', '.dropdown-item',
|
||||
() => this.$widget.find("[data-toggle='dropdown']").dropdown('toggle'));
|
||||
|
||||
this.$widget.find(".global-menu-button-update-available").append(
|
||||
this.updateAvailableWidget.render()
|
||||
);
|
||||
|
||||
this.$updateToLatestVersionButton = this.$widget.find(".update-to-latest-version-button");
|
||||
|
||||
this.updateVersionStatus();
|
||||
|
||||
setInterval(() => this.updateVersionStatus(), 8 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
async updateVersionStatus() {
|
||||
const latestVersion = await this.fetchLatestVersion();
|
||||
|
||||
this.updateAvailableWidget.updateVersionStatus(latestVersion);
|
||||
|
||||
this.$updateToLatestVersionButton.toggle(latestVersion > glob.triliumVersion);
|
||||
this.$updateToLatestVersionButton.find(".version-text").text(`Version ${latestVersion} is available, click to download.`);
|
||||
}
|
||||
|
||||
async fetchLatestVersion() {
|
||||
const RELEASES_API_URL = "https://api.github.com/repos/zadam/trilium/releases/latest";
|
||||
|
||||
const resp = await fetch(RELEASES_API_URL);
|
||||
const data = await resp.json();
|
||||
|
||||
return data.tag_name.substring(1);
|
||||
}
|
||||
|
||||
downloadLatestVersionCommand() {
|
||||
window.open("https://github.com/zadam/trilium/releases/latest");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,12 @@ export default class OpenNoteButtonWidget extends ButtonWidget {
|
||||
}
|
||||
|
||||
this.icon(note.getIcon());
|
||||
this.title(note.title);
|
||||
this.title(() => {
|
||||
const n = froca.getNoteFromCache(noteId);
|
||||
|
||||
// always fresh, always decoded (when protected session is available)
|
||||
return n.title;
|
||||
});
|
||||
|
||||
this.refreshIcon();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
|
||||
const TPL = `
|
||||
<div style="display: none;">
|
||||
<style>
|
||||
.global-menu-button-update-available-button {
|
||||
width: 21px !important;
|
||||
height: 21px !important;
|
||||
padding: 0 !important;
|
||||
|
||||
border-radius: 8px;
|
||||
transform: scale(0.9);
|
||||
border: none;
|
||||
opacity: 0.8;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.global-menu-button-wrapper:hover .global-menu-button-update-available-button {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<span class="bx bx-sync global-menu-button-update-available-button" title="Update available"></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class UpdateAvailableWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
}
|
||||
|
||||
updateVersionStatus(latestVersion) {
|
||||
this.$widget.toggle(latestVersion > glob.triliumVersion);
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,6 @@ export default class SplitNoteContainer extends FlexContainer {
|
||||
const $renderedWidget = widget.render();
|
||||
|
||||
$renderedWidget.attr("data-ntx-id", noteContext.ntxId);
|
||||
$renderedWidget.addClass("note-split");
|
||||
|
||||
$renderedWidget.on('click', () => appContext.tabManager.activateNoteContext(noteContext.ntxId));
|
||||
|
||||
this.$widget.append($renderedWidget);
|
||||
|
||||
@@ -42,8 +42,7 @@ export default class HistoryNavigationWidget extends BasicWidget {
|
||||
this.$forwardInHistory = this.$widget.find("[data-trigger-command='forwardInNoteHistory']");
|
||||
this.$forwardInHistory.on('contextmenu', contextMenuHandler);
|
||||
|
||||
const electron = utils.dynamicRequire('electron');
|
||||
this.webContents = electron.remote.getCurrentWindow().webContents;
|
||||
this.webContents = utils.dynamicRequire('@electron/remote').webContents;
|
||||
|
||||
// without this the history is preserved across frontend reloads
|
||||
this.webContents.clearHistory();
|
||||
|
||||
@@ -661,7 +661,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
extraClasses.push("protected");
|
||||
}
|
||||
|
||||
if (note.getParentNoteIds().length > 1) {
|
||||
if (note.isShared()) {
|
||||
extraClasses.push("shared");
|
||||
}
|
||||
else if (note.getParentNoteIds().length > 1) {
|
||||
const notSearchParents = note.getParentNoteIds()
|
||||
.map(noteId => froca.notes[noteId])
|
||||
.filter(note => !!note)
|
||||
@@ -1014,8 +1017,14 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
for (const ecBranch of loadResults.getBranches()) {
|
||||
// adding noteId itself to update all potential clones
|
||||
noteIdsToUpdate.add(ecBranch.noteId);
|
||||
if (ecBranch.parentNoteId === 'share') {
|
||||
// all shared notes have a sign in the tree, even the descendants of shared notes
|
||||
noteIdsToReload.add(ecBranch.noteId);
|
||||
}
|
||||
else {
|
||||
// adding noteId itself to update all potential clones
|
||||
noteIdsToUpdate.add(ecBranch.noteId);
|
||||
}
|
||||
|
||||
for (const node of this.getNodesByBranch(ecBranch)) {
|
||||
if (ecBranch.isDeleted) {
|
||||
|
||||
@@ -133,9 +133,7 @@ export default class NoteTypeWidget extends NoteContextAwareWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
await server.put('notes/' + this.noteId
|
||||
+ '/type/' + encodeURIComponent(type)
|
||||
+ '/mime/' + encodeURIComponent(mime));
|
||||
await server.put('notes/' + this.noteId + '/type', { type, mime });
|
||||
}
|
||||
|
||||
async confirmChangeIfContent() {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import FlexContainer from "./containers/flex_container.js";
|
||||
|
||||
export default class NoteWrapperWidget extends FlexContainer {
|
||||
constructor() {
|
||||
super('column');
|
||||
|
||||
this.css("flex-grow", "1")
|
||||
.collapsible();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
super.doRender();
|
||||
|
||||
this.$widget.addClass("note-split");
|
||||
}
|
||||
|
||||
setNoteContextEvent({noteContext}) {
|
||||
this.refresh(noteContext);
|
||||
}
|
||||
|
||||
noteSwitchedAndActivatedEvent({noteContext}) {
|
||||
this.refresh(noteContext);
|
||||
}
|
||||
|
||||
noteSwitchedEvent({noteContext}) {
|
||||
this.refresh(noteContext);
|
||||
}
|
||||
|
||||
activeContextChangedEvent({noteContext}) {
|
||||
this.refresh(noteContext);
|
||||
}
|
||||
|
||||
refresh(noteContext) {
|
||||
this.$widget.toggleClass("full-content-width",
|
||||
['image', 'mermaid', 'book', 'render'].includes(noteContext?.note?.type)
|
||||
|| !!noteContext?.note?.hasLabel('fullContentWidth')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,89 +1,28 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import protectedSessionService from "../services/protected_session.js";
|
||||
import SwitchWidget from "./switch.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="protected-note-switch">
|
||||
<style>
|
||||
/* The switch - the box around the slider */
|
||||
.protected-note-switch .switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.protected-note-switch .slider {
|
||||
border-radius: 24px;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--more-accented-background-color);
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.protected-note-switch .slider:before {
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: var(--main-background-color);
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.protected-note-switch .slider.checked {
|
||||
background-color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.protected-note-switch .slider.checked:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="protect-button">
|
||||
Protect the note
|
||||
|
||||
|
||||
|
||||
<span title="Note is not protected, click to make it protected">
|
||||
<label class="switch">
|
||||
<span class="slider"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="unprotect-button">
|
||||
Unprotect the note
|
||||
|
||||
|
||||
|
||||
<span title="Note is protected, click to make it unprotected">
|
||||
<label class="switch">
|
||||
<span class="slider checked"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class ProtectedNoteSwitchWidget extends NoteContextAwareWidget {
|
||||
export default class ProtectedNoteSwitchWidget extends SwitchWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
super.doRender();
|
||||
|
||||
this.$protectButton = this.$widget.find(".protect-button");
|
||||
this.$protectButton.on('click', () => protectedSessionService.protectNote(this.noteId, true, false));
|
||||
this.$switchOnName.text("Protect the note");
|
||||
this.$switchOnButton.attr("title", "Note is not protected, click to make it protected");
|
||||
|
||||
this.$unprotectButton = this.$widget.find(".unprotect-button");
|
||||
this.$unprotectButton.on('click', () => protectedSessionService.protectNote(this.noteId, false, false));
|
||||
this.$switchOffName.text("Unprotect the note");
|
||||
this.$switchOffButton.attr("title", "Note is protected, click to make it unprotected");
|
||||
}
|
||||
|
||||
switchOn() {
|
||||
protectedSessionService.protectNote(this.noteId, true, false);
|
||||
}
|
||||
|
||||
switchOff() {
|
||||
protectedSessionService.protectNote(this.noteId, false, false)
|
||||
}
|
||||
|
||||
refreshWithNote(note) {
|
||||
this.$protectButton.toggle(!note.isProtected);
|
||||
this.$unprotectButton.toggle(!!note.isProtected);
|
||||
this.$switchOn.toggle(!note.isProtected);
|
||||
this.$switchOff.toggle(!!note.isProtected);
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
|
||||
@@ -52,6 +52,20 @@ export default class QuickSearchWidget extends BasicWidget {
|
||||
|
||||
this.$widget.find('.input-group-prepend').on('shown.bs.dropdown', () => this.search());
|
||||
|
||||
if(utils.isMobile()) {
|
||||
this.$searchString.keydown(e =>{
|
||||
if(e.which==13) {
|
||||
if (this.$dropdownMenu.is(":visible")) {
|
||||
this.search(); // just update already visible dropdown
|
||||
} else {
|
||||
this.$dropdownToggle.dropdown('show');
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
utils.bindElShortcut(this.$searchString, 'return', () => {
|
||||
if (this.$dropdownMenu.is(":visible")) {
|
||||
this.search(); // just update already visible dropdown
|
||||
|
||||
@@ -3,6 +3,7 @@ import NoteTypeWidget from "../note_type.js";
|
||||
import ProtectedNoteSwitchWidget from "../protected_note_switch.js";
|
||||
import EditabilitySelectWidget from "../editability_select.js";
|
||||
import BookmarkSwitchWidget from "../bookmark_switch.js";
|
||||
import SharedSwitchWidget from "../shared_switch.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="basic-properties-widget">
|
||||
@@ -36,6 +37,8 @@ const TPL = `
|
||||
</div>
|
||||
|
||||
<div class="bookmark-switch-container"></div>
|
||||
|
||||
<div class="shared-switch-container"></div>
|
||||
</div>`;
|
||||
|
||||
export default class BasicPropertiesWidget extends NoteContextAwareWidget {
|
||||
@@ -46,12 +49,14 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget {
|
||||
this.protectedNoteSwitchWidget = new ProtectedNoteSwitchWidget().contentSized();
|
||||
this.editabilitySelectWidget = new EditabilitySelectWidget().contentSized();
|
||||
this.bookmarkSwitchWidget = new BookmarkSwitchWidget().contentSized();
|
||||
this.sharedSwitchWidget = new SharedSwitchWidget().contentSized();
|
||||
|
||||
this.child(
|
||||
this.noteTypeWidget,
|
||||
this.protectedNoteSwitchWidget,
|
||||
this.editabilitySelectWidget,
|
||||
this.bookmarkSwitchWidget
|
||||
this.bookmarkSwitchWidget,
|
||||
this.sharedSwitchWidget
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,6 +88,7 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget {
|
||||
this.$widget.find(".protected-note-switch-container").append(this.protectedNoteSwitchWidget.render());
|
||||
this.$widget.find(".editability-select-container").append(this.editabilitySelectWidget.render());
|
||||
this.$widget.find(".bookmark-switch-container").append(this.bookmarkSwitchWidget.render());
|
||||
this.$widget.find(".shared-switch-container").append(this.sharedSwitchWidget.render());
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import options from "../services/options.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="shared-info-widget alert alert-warning">
|
||||
<style>
|
||||
.shared-info-widget {
|
||||
margin: 10px;
|
||||
contain: none;
|
||||
padding: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
<span class="share-text"></span> <a class="share-link external"></a>. For help visit <a href="https://github.com/zadam/trilium/wiki/Sharing">wiki</a>.
|
||||
</div>`;
|
||||
|
||||
export default class SharedInfoWidget extends NoteContextAwareWidget {
|
||||
isEnabled() {
|
||||
return super.isEnabled() && this.noteId !== 'share' && this.note.hasAncestor('share');
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$shareLink = this.$widget.find(".share-link");
|
||||
this.$shareText = this.$widget.find(".share-text");
|
||||
this.contentSized();
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
const syncServerHost = options.get("syncServerHost");
|
||||
let link;
|
||||
|
||||
const shareId = note.getOwnedLabelValue('shareAlias') || note.noteId;
|
||||
|
||||
if (syncServerHost) {
|
||||
link = syncServerHost + "/share/" + shareId;
|
||||
this.$shareText.text("This note is shared publicly on");
|
||||
}
|
||||
else {
|
||||
link = location.protocol + '//' + location.host + location.pathname + "share/" + shareId;
|
||||
this.$shareText.text("This note is shared locally on");
|
||||
}
|
||||
|
||||
this.$shareLink.attr("href", link).text(link);
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.getAttributes().find(attr => attr.name.startsWith("share") && attributeService.isAffecting(attr, this.note))) {
|
||||
this.refresh();
|
||||
}
|
||||
else if (loadResults.getBranches().find(branch => branch.noteId === this.noteId)) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import SwitchWidget from "./switch.js";
|
||||
import branchService from "../services/branches.js";
|
||||
import server from "../services/server.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
export default class SharedSwitchWidget extends SwitchWidget {
|
||||
isEnabled() {
|
||||
return super.isEnabled() && this.noteId !== 'root' && this.noteId !== 'share';
|
||||
}
|
||||
|
||||
doRender() {
|
||||
super.doRender();
|
||||
|
||||
this.$switchOnName.text("Shared");
|
||||
this.$switchOnButton.attr("title", "Share the note");
|
||||
|
||||
this.$switchOffName.text("Shared");
|
||||
this.$switchOffButton.attr("title", "Unshare the note");
|
||||
|
||||
this.$helpButton.attr("data-help-page", "Sharing").show();
|
||||
this.$helpButton.on('click', e => utils.openHelp(e));
|
||||
}
|
||||
|
||||
switchOn() {
|
||||
branchService.cloneNoteTo(this.noteId, 'share');
|
||||
}
|
||||
|
||||
async switchOff() {
|
||||
const shareBranch = this.note.getParentBranches().find(b => b.parentNoteId === 'share');
|
||||
|
||||
if (!shareBranch) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.note.getParentBranches().length === 1) {
|
||||
const confirmDialog = await import('../dialogs/confirm.js');
|
||||
|
||||
const text = "This note exists only as a shared note, unsharing would delete it. Do you want to continue and thus delete this note?";
|
||||
|
||||
if (!await confirmDialog.confirm(text)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await server.remove(`branches/${shareBranch.branchId}?taskId=no-progress-reporting`);
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
const isShared = note.hasAncestor('share');
|
||||
const canBeUnshared = isShared && note.getParentBranches().find(b => b.parentNoteId === 'share');
|
||||
const switchDisabled = isShared && !canBeUnshared;
|
||||
|
||||
this.$switchOn.toggle(!isShared);
|
||||
this.$switchOff.toggle(!!isShared);
|
||||
|
||||
if (switchDisabled) {
|
||||
this.$widget.attr("title", "Note cannot be unshared here because it is shared through inheritance from an ancestor.");
|
||||
this.$switchOff.addClass("switch-disabled");
|
||||
}
|
||||
else {
|
||||
this.$widget.removeAttr("title");
|
||||
this.$switchOff.removeClass("switch-disabled");
|
||||
}
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.getBranches().find(b => b.noteId === this.noteId)) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="switch-widget">
|
||||
<style>
|
||||
.switch-widget {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* The switch - the box around the slider */
|
||||
.switch-widget .switch {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.switch-on, .switch-off {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.switch-widget .slider {
|
||||
border-radius: 24px;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--more-accented-background-color);
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.switch-widget .slider:before {
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: var(--main-background-color);
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.switch-widget .slider.checked {
|
||||
background-color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.switch-widget .slider.checked:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
.switch-widget .switch-disabled {
|
||||
opacity: 70%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.switch-widget .switch-help-button {
|
||||
font-weight: 900;
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="switch-on">
|
||||
<span class="switch-on-name"></span>
|
||||
|
||||
|
||||
|
||||
<span class="switch-on-button">
|
||||
<label class="switch">
|
||||
<span class="slider"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="switch-off">
|
||||
<span class="switch-off-name"></span>
|
||||
|
||||
|
||||
|
||||
<span class="switch-off-button">
|
||||
<label class="switch">
|
||||
<span class="slider checked"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button class="switch-help-button" type="button" data-help-page="" title="Open help page" style="display: none;">?</button>
|
||||
</div>`;
|
||||
|
||||
export default class SwitchWidget extends NoteContextAwareWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$switchOn = this.$widget.find(".switch-on");
|
||||
this.$switchOnName = this.$widget.find(".switch-on-name");
|
||||
this.$switchOnButton = this.$widget.find(".switch-on-button");
|
||||
|
||||
this.$switchOnButton.on('click', () => this.switchOn());
|
||||
|
||||
this.$switchOff = this.$widget.find(".switch-off");
|
||||
this.$switchOffName = this.$widget.find(".switch-off-name");
|
||||
this.$switchOffButton = this.$widget.find(".switch-off-button");
|
||||
|
||||
this.$switchOffButton.on('click', () => this.switchOff());
|
||||
|
||||
this.$helpButton = this.$widget.find(".switch-help-button");
|
||||
|
||||
}
|
||||
|
||||
switchOff() {}
|
||||
switchOn() {}
|
||||
}
|
||||
@@ -187,6 +187,14 @@ const TAB_ROW_TPL = `
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab-row-widget .note-tab:hover .note-tab-wrapper {
|
||||
background-color: var(--inactive-tab-hover-background-color);
|
||||
}
|
||||
|
||||
.tab-row-widget .note-tab[active]:hover .note-tab-wrapper {
|
||||
background-color: var(--active-tab-hover-background-color);
|
||||
}
|
||||
|
||||
.tab-row-widget .note-tab .note-tab-close:hover {
|
||||
background-color: var(--hover-item-background-color);
|
||||
color: var(--hover-item-text-color);
|
||||
|
||||
@@ -49,13 +49,13 @@ export default class TitleBarButtonsWidget extends BasicWidget {
|
||||
|
||||
$minimizeBtn.on('click', () => {
|
||||
$minimizeBtn.trigger('blur');
|
||||
const {remote} = utils.dynamicRequire('electron');
|
||||
const remote = utils.dynamicRequire('@electron/remote');
|
||||
remote.BrowserWindow.getFocusedWindow().minimize();
|
||||
});
|
||||
|
||||
$maximizeBtn.on('click', () => {
|
||||
$maximizeBtn.trigger('blur');
|
||||
const {remote} = utils.dynamicRequire('electron');
|
||||
const remote = utils.dynamicRequire('@electron/remote');
|
||||
const focusedWindow = remote.BrowserWindow.getFocusedWindow();
|
||||
|
||||
if (focusedWindow.isMaximized()) {
|
||||
@@ -67,7 +67,7 @@ export default class TitleBarButtonsWidget extends BasicWidget {
|
||||
|
||||
$closeBtn.on('click', () => {
|
||||
$closeBtn.trigger('blur');
|
||||
const {remote} = utils.dynamicRequire('electron');
|
||||
const remote = utils.dynamicRequire('@electron/remote');
|
||||
remote.BrowserWindow.getFocusedWindow().close();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,20 +14,10 @@ const TPL = `
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.trilium-api-docs-button {
|
||||
/*display: none;*/
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.note-detail-code-editor {
|
||||
min-height: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button class="btn bx bx-help-circle trilium-api-docs-button icon-button floating-button"
|
||||
title="Open Trilium API docs"></button>
|
||||
|
||||
<div class="note-detail-code-editor"></div>
|
||||
|
||||
@@ -37,6 +27,13 @@ const TPL = `
|
||||
Execute <kbd data-command="runActiveNote"></kbd>
|
||||
</button>
|
||||
|
||||
<button class="no-print trilium-api-docs-button btn btn-sm"
|
||||
title="Open Trilium API docs">
|
||||
<span class="bx bx-help-circle"></span>
|
||||
|
||||
API docs
|
||||
</button>
|
||||
|
||||
<button class="no-print save-to-note-button btn btn-sm">
|
||||
|
||||
<span class="bx bx-save"></span>
|
||||
|
||||
@@ -84,5 +84,11 @@ export default class EmptyTypeWidget extends TypeWidget {
|
||||
.on('click', () => this.triggerCommand('hoistNote', {noteId: workspaceNote.noteId}))
|
||||
);
|
||||
}
|
||||
|
||||
if (workspaceNotes.length === 0) {
|
||||
this.$autoComplete
|
||||
.trigger('focus')
|
||||
.trigger('select');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
body {
|
||||
font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif;
|
||||
}
|
||||
|
||||
#layout {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
#menu {
|
||||
padding: 25px;
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#menu p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#menu > p {
|
||||
font-weight: bold;
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
#menu ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
#main {
|
||||
flex-basis: 0;
|
||||
flex-grow: 3;
|
||||
}
|
||||
|
||||
#title {
|
||||
margin: 0;
|
||||
padding: 20px 20px 0 20px;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: anywhere;
|
||||
}
|
||||
|
||||
iframe.pdf-view {
|
||||
width: 100%;
|
||||
height: 800px;
|
||||
}
|
||||
|
||||
#menuButton {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 8px;
|
||||
left: 5px;
|
||||
width: 1.4em;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #aaa;
|
||||
font-size: 2rem;
|
||||
z-index: 10;
|
||||
height: auto;
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#menuButton::after {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
left: 1px;
|
||||
}
|
||||
|
||||
@media (max-width: 48em) {
|
||||
#layout.navMenu #menu {
|
||||
display: block;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
#menuButton {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#layout.navMenu #main {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#title {
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
#layout.navMenu #menuButton::after {
|
||||
content: "«";
|
||||
}
|
||||
|
||||
#menuButton::after {
|
||||
content: "»";
|
||||
}
|
||||
|
||||
#menu {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -954,3 +954,7 @@ input {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.note-split.full-content-width {
|
||||
max-width: 999999px;
|
||||
}
|
||||
|
||||
@@ -54,9 +54,11 @@
|
||||
--launcher-pane-text-color: #AAAAAA;
|
||||
|
||||
--active-tab-background-color: #666;
|
||||
--active-tab-hover-background-color: #737373;
|
||||
--active-tab-text-color: #ccc;
|
||||
|
||||
--inactive-tab-background-color: #444;
|
||||
--inactive-tab-hover-background-color: #525252;
|
||||
--inactive-tab-text-color: #bbb;
|
||||
|
||||
--scrollbar-border-color: #888;
|
||||
|
||||
@@ -58,9 +58,11 @@ html {
|
||||
--launcher-pane-text-color: #333;
|
||||
|
||||
--active-tab-background-color: #ddd;
|
||||
--active-tab-hover-background-color: #d1d1d1;
|
||||
--active-tab-text-color: black;
|
||||
|
||||
--inactive-tab-background-color: #f0f0f0;
|
||||
--inactive-tab-hover-background-color: #e3e3e3;
|
||||
--inactive-tab-text-color: #666;
|
||||
|
||||
--scrollbar-border-color: #ddd;
|
||||
|
||||
@@ -46,11 +46,11 @@ span.fancytree-node.fancytree-hide {
|
||||
|
||||
.fancytree-node:not(.fancytree-loading) .fancytree-expander:before {
|
||||
font-family: 'boxicons' !important;
|
||||
content: "\ea50"; /* lookup code for "chevron-right" in boxicons.css */
|
||||
speak: none;
|
||||
font-size: x-large;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
content: "\e9b2";
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin-right: 5px;
|
||||
@@ -102,7 +102,7 @@ ul.fancytree-container li {
|
||||
|
||||
.fancytree-node.fancytree-expanded .fancytree-expander:before {
|
||||
font-family: 'boxicons' !important;
|
||||
content: "\e9ac";
|
||||
content: "\ea4a"; /* lookup code for "chevron-down" in boxicons.css */
|
||||
}
|
||||
|
||||
/** some common text styling for cssClass label */
|
||||
@@ -135,7 +135,13 @@ span.fancytree-node.protected > span.fancytree-custom-icon {
|
||||
}
|
||||
|
||||
span.fancytree-node.multiple-parents .fancytree-title::after {
|
||||
content: " *"
|
||||
content: " *";
|
||||
}
|
||||
|
||||
span.fancytree-node.shared .fancytree-title::after {
|
||||
font-family: 'boxicons' !important;
|
||||
font-size: smaller;
|
||||
content: " \ec03"; /* lookup code for "share-alt" in boxicons.css */
|
||||
}
|
||||
|
||||
span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-title {
|
||||
|
||||
+10
-7
@@ -1,19 +1,22 @@
|
||||
"use strict";
|
||||
|
||||
const keyboardActions = require('../../services/keyboard_actions');
|
||||
const sql = require('../../services/sql');
|
||||
const becca = require('../../becca/becca');
|
||||
|
||||
function getKeyboardActions() {
|
||||
return keyboardActions.getKeyboardActions();
|
||||
}
|
||||
|
||||
function getShortcutsForNotes() {
|
||||
return sql.getMap(`
|
||||
SELECT value, noteId
|
||||
FROM attributes
|
||||
WHERE isDeleted = 0
|
||||
AND type = 'label'
|
||||
AND name = 'keyboardShortcut'`);
|
||||
const attrs = becca.findAttributes('label', 'keyboardShortcut');
|
||||
|
||||
const map = {};
|
||||
|
||||
for (const attr of attrs) {
|
||||
map[attr.value] = attr.noteId;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
+134
-1
@@ -1,6 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const becca = require("../../becca/becca");
|
||||
const { JSDOM } = require("jsdom");
|
||||
|
||||
function buildDescendantCountMap() {
|
||||
const noteIdToCountMap = {};
|
||||
@@ -184,7 +185,139 @@ function getTreeMap(req) {
|
||||
};
|
||||
}
|
||||
|
||||
function removeImages(document) {
|
||||
const images = document.getElementsByTagName('img');
|
||||
while (images.length > 0) {
|
||||
images[0].parentNode.removeChild(images[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const EXCERPT_CHAR_LIMIT = 200;
|
||||
|
||||
function findExcerpts(sourceNote, referencedNoteId) {
|
||||
const html = sourceNote.getContent();
|
||||
const document = new JSDOM(html).window.document;
|
||||
|
||||
const excerpts = [];
|
||||
|
||||
removeImages(document);
|
||||
|
||||
for (const linkEl of document.querySelectorAll("a")) {
|
||||
const href = linkEl.getAttribute("href");
|
||||
|
||||
if (!href || !href.endsWith(referencedNoteId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
linkEl.classList.add("backlink-link");
|
||||
|
||||
let centerEl = linkEl;
|
||||
|
||||
while (centerEl.tagName !== 'BODY' && centerEl.parentElement.textContent.length <= EXCERPT_CHAR_LIMIT) {
|
||||
centerEl = centerEl.parentElement;
|
||||
}
|
||||
|
||||
const excerptEls = [centerEl];
|
||||
let excerptLength = centerEl.textContent.length;
|
||||
let left = centerEl;
|
||||
let right = centerEl;
|
||||
|
||||
while (excerptLength < EXCERPT_CHAR_LIMIT) {
|
||||
let added = false;
|
||||
|
||||
const prev = left.previousElementSibling;
|
||||
|
||||
if (prev) {
|
||||
const prevText = prev.textContent;
|
||||
|
||||
if (prevText.length + excerptLength > EXCERPT_CHAR_LIMIT) {
|
||||
const prefix = prevText.substr(prevText.length - (EXCERPT_CHAR_LIMIT - excerptLength));
|
||||
|
||||
const textNode = document.createTextNode("…" + prefix);
|
||||
excerptEls.unshift(textNode);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
left = prev;
|
||||
excerptEls.unshift(left);
|
||||
excerptLength += prevText.length;
|
||||
added = true;
|
||||
}
|
||||
|
||||
const next = right.nextElementSibling;
|
||||
|
||||
if (next) {
|
||||
const nextText = next.textContent;
|
||||
|
||||
if (nextText.length + excerptLength > EXCERPT_CHAR_LIMIT) {
|
||||
const suffix = nextText.substr(nextText.length - (EXCERPT_CHAR_LIMIT - excerptLength));
|
||||
|
||||
const textNode = document.createTextNode(suffix + "…");
|
||||
excerptEls.push(textNode);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
right = next;
|
||||
excerptEls.push(right);
|
||||
excerptLength += nextText.length;
|
||||
added = true;
|
||||
}
|
||||
|
||||
if (!added) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const excerptWrapper = document.createElement('div');
|
||||
excerptWrapper.classList.add("ck-content");
|
||||
excerptWrapper.classList.add("backlink-excerpt");
|
||||
|
||||
for (const childEl of excerptEls) {
|
||||
excerptWrapper.appendChild(childEl);
|
||||
}
|
||||
|
||||
excerpts.push(excerptWrapper.outerHTML);
|
||||
}
|
||||
return excerpts;
|
||||
}
|
||||
|
||||
function getBacklinks(req) {
|
||||
const {noteId} = req.params;
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
return [404, `Note ${noteId} was not found`];
|
||||
}
|
||||
|
||||
let backlinks = note.getTargetRelations();
|
||||
|
||||
let backlinksWithExcerptCount = 0;
|
||||
|
||||
return backlinks.map(backlink => {
|
||||
const sourceNote = backlink.note;
|
||||
|
||||
if (sourceNote.type !== 'text' || backlinksWithExcerptCount > 50) {
|
||||
return {
|
||||
noteId: sourceNote.noteId,
|
||||
relationName: backlink.name
|
||||
};
|
||||
}
|
||||
|
||||
backlinksWithExcerptCount++;
|
||||
|
||||
const excerpts = findExcerpts(sourceNote, noteId);
|
||||
|
||||
return {
|
||||
noteId: sourceNote.noteId,
|
||||
excerpts
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getLinkMap,
|
||||
getTreeMap
|
||||
getTreeMap,
|
||||
getBacklinks
|
||||
};
|
||||
|
||||
+21
-6
@@ -73,7 +73,7 @@ function deleteNote(req) {
|
||||
|
||||
const taskContext = TaskContext.getInstance(taskId, 'delete-notes');
|
||||
|
||||
for (const branch of note.getBranches()) {
|
||||
for (const branch of note.getParentBranches()) {
|
||||
noteService.deleteBranch(branch, deleteId, taskContext);
|
||||
}
|
||||
|
||||
@@ -120,9 +120,8 @@ function protectNote(req) {
|
||||
|
||||
function setNoteTypeMime(req) {
|
||||
// can't use [] destructuring because req.params is not iterable
|
||||
const noteId = req.params[0];
|
||||
const type = req.params[1];
|
||||
const mime = req.params[2];
|
||||
const {noteId} = req.params;
|
||||
const {type, mime} = req.body;
|
||||
|
||||
const note = becca.getNote(noteId);
|
||||
note.type = type;
|
||||
@@ -240,7 +239,7 @@ function getDeleteNotesPreview(req) {
|
||||
|
||||
const note = branch.getNote();
|
||||
|
||||
if (deleteAllClones || note.getBranches().length <= branchCountToDelete[branch.branchId]) {
|
||||
if (deleteAllClones || note.getParentBranches().length <= branchCountToDelete[branch.branchId]) {
|
||||
noteIdsToBeDeleted.add(note.noteId);
|
||||
|
||||
for (const childBranch of note.getChildBranches()) {
|
||||
@@ -303,6 +302,21 @@ function uploadModifiedFile(req) {
|
||||
note.setContent(fileContent);
|
||||
}
|
||||
|
||||
function getBacklinkCount(req) {
|
||||
const {noteId} = req.params;
|
||||
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
return [404, "Not found"];
|
||||
}
|
||||
else {
|
||||
return {
|
||||
count: note.getTargetRelations().length
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNote,
|
||||
updateNote,
|
||||
@@ -317,5 +331,6 @@ module.exports = {
|
||||
duplicateSubtree,
|
||||
eraseDeletedNotesNow,
|
||||
getDeleteNotesPreview,
|
||||
uploadModifiedFile
|
||||
uploadModifiedFile,
|
||||
getBacklinkCount
|
||||
};
|
||||
|
||||
@@ -54,7 +54,8 @@ const ALLOWED_OPTIONS = new Set([
|
||||
'dailyBackupEnabled',
|
||||
'weeklyBackupEnabled',
|
||||
'monthlyBackupEnabled',
|
||||
'maxContentWidth'
|
||||
'maxContentWidth',
|
||||
'compressImages'
|
||||
]);
|
||||
|
||||
function getOptions() {
|
||||
|
||||
@@ -38,7 +38,7 @@ function run(req) {
|
||||
}
|
||||
|
||||
function getBundlesWithLabel(label, value) {
|
||||
const notes = attributeService.getNotesWithLabel(label, value);
|
||||
const notes = attributeService.getNotesWithLabelFast(label, value);
|
||||
|
||||
const bundles = [];
|
||||
|
||||
|
||||
@@ -17,7 +17,17 @@ function uploadImage(req) {
|
||||
|
||||
const parentNote = dateNoteService.getDateNote(req.headers['x-local-date']);
|
||||
|
||||
const {noteId} = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true);
|
||||
const {note, noteId} = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true);
|
||||
|
||||
const labelsStr = req.headers['x-labels'];
|
||||
|
||||
if (labelsStr?.trim()) {
|
||||
const labels = JSON.parse(labelsStr);
|
||||
|
||||
for (const {name, value} of labels) {
|
||||
note.setLabel(attributeService.sanitizeAttributeName(name), value);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
noteId: noteId
|
||||
|
||||
+3
-1
@@ -9,6 +9,7 @@ const log = require('../services/log');
|
||||
const env = require('../services/env');
|
||||
const utils = require('../services/utils');
|
||||
const protectedSessionService = require("../services/protected_session");
|
||||
const packageJson = require('../../package.json');
|
||||
|
||||
function index(req, res) {
|
||||
const options = optionService.getOptionsMap();
|
||||
@@ -36,7 +37,8 @@ function index(req, res) {
|
||||
isMainWindow: !req.query.extra,
|
||||
extraHoistedNoteId: req.query.extraHoistedNoteId,
|
||||
isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(),
|
||||
maxContentWidth: parseInt(options.maxContentWidth)
|
||||
maxContentWidth: parseInt(options.maxContentWidth),
|
||||
triliumVersion: packageJson.version
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ const keysRoute = require('./api/keys');
|
||||
const backendLogRoute = require('./api/backend_log');
|
||||
const statsRoute = require('./api/stats');
|
||||
const fontsRoute = require('./api/fonts');
|
||||
const shareRoutes = require('../share/routes');
|
||||
|
||||
const log = require('../services/log');
|
||||
const express = require('express');
|
||||
@@ -212,13 +213,14 @@ function register(app) {
|
||||
apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote);
|
||||
apiRoute(PUT, '/api/notes/:noteId/sort-children', notesApiRoute.sortChildNotes);
|
||||
apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectNote);
|
||||
apiRoute(PUT, /\/api\/notes\/(.*)\/type\/(.*)\/mime\/(.*)/, notesApiRoute.setNoteTypeMime);
|
||||
apiRoute(PUT, '/api/notes/:noteId/type', notesApiRoute.setNoteTypeMime);
|
||||
apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions);
|
||||
apiRoute(DELETE, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.eraseAllNoteRevisions);
|
||||
apiRoute(GET, '/api/notes/:noteId/revisions/:noteRevisionId', noteRevisionsApiRoute.getNoteRevision);
|
||||
apiRoute(DELETE, '/api/notes/:noteId/revisions/:noteRevisionId', noteRevisionsApiRoute.eraseNoteRevision);
|
||||
route(GET, '/api/notes/:noteId/revisions/:noteRevisionId/download', [auth.checkApiAuthOrElectron], noteRevisionsApiRoute.downloadNoteRevision);
|
||||
apiRoute(PUT, '/api/notes/:noteId/restore-revision/:noteRevisionId', noteRevisionsApiRoute.restoreNoteRevision);
|
||||
apiRoute(GET, '/api/notes/:noteId/backlink-count', notesApiRoute.getBacklinkCount);
|
||||
apiRoute(POST, '/api/notes/relation-map', notesApiRoute.getRelationMap);
|
||||
apiRoute(POST, '/api/notes/erase-deleted-notes-now', notesApiRoute.eraseDeletedNotesNow);
|
||||
apiRoute(PUT, '/api/notes/:noteId/change-title', notesApiRoute.changeTitle);
|
||||
@@ -259,6 +261,7 @@ function register(app) {
|
||||
|
||||
apiRoute(POST, '/api/note-map/:noteId/tree', noteMapRoute.getTreeMap);
|
||||
apiRoute(POST, '/api/note-map/:noteId/link', noteMapRoute.getLinkMap);
|
||||
apiRoute(GET, '/api/note-map/:noteId/backlinks', noteMapRoute.getBacklinks);
|
||||
|
||||
apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote);
|
||||
apiRoute(GET, '/api/special-notes/date/:date', specialNotesRoute.getDateNote);
|
||||
@@ -367,6 +370,8 @@ function register(app) {
|
||||
|
||||
route(GET, '/api/fonts', [auth.checkApiAuthOrElectron], fontsRoute.getFontCss);
|
||||
|
||||
shareRoutes.register(router);
|
||||
|
||||
app.use('', router);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user