mirror of
https://github.com/zadam/trilium.git
synced 2025-11-02 03:16:11 +01:00
Compare commits
74 Commits
v0.48.6
...
algolia_v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baed93e749 | ||
|
|
657496ea37 | ||
|
|
034aaa7209 | ||
|
|
a81ea3771f | ||
|
|
4ceba8cc6e | ||
|
|
d9550dd59b | ||
|
|
97f7fe7b18 | ||
|
|
a810c08c02 | ||
|
|
ab550a1e8d | ||
|
|
08e8047d8a | ||
|
|
67da877135 | ||
|
|
263b7a84bb | ||
|
|
9d18bebb13 | ||
|
|
2d339dec6b | ||
|
|
b78ab1ee02 | ||
|
|
2f5f116345 | ||
|
|
64f1671566 | ||
|
|
26bcfe5160 | ||
|
|
89c04e6b6b | ||
|
|
e079359c15 | ||
|
|
c4ab6b4866 | ||
|
|
630d9f2e45 | ||
|
|
bbceb6251a | ||
|
|
89f117da5b | ||
|
|
40fb4ff56b | ||
|
|
d64c14482b | ||
|
|
61f197dd81 | ||
|
|
564366861e | ||
|
|
1ee2abcc42 | ||
|
|
f4242b4096 | ||
|
|
d59542dd6f | ||
|
|
211ff90ee8 | ||
|
|
8c11d022fb | ||
|
|
24210ef80c | ||
|
|
3f40a52f65 | ||
|
|
df4cf80be4 | ||
|
|
bc854ee149 | ||
|
|
b23ead8097 | ||
|
|
886fdf7cd6 | ||
|
|
2135aa058e | ||
|
|
de20183a22 | ||
|
|
42b5437c87 | ||
|
|
67542f448d | ||
|
|
ae29c6bac4 | ||
|
|
1dce96b4c1 | ||
|
|
08e9b59696 | ||
|
|
db9e35a7e1 | ||
|
|
fe605c012a | ||
|
|
7a383a1314 | ||
|
|
5290aab781 | ||
|
|
86c3bbe5a2 | ||
|
|
4c7c53d8c8 | ||
|
|
21854b4a04 | ||
|
|
83f125a79f | ||
|
|
e36bc42519 | ||
|
|
15ac81627c | ||
|
|
57fae2c8c6 | ||
|
|
87b76abef9 | ||
|
|
d345b7ed56 | ||
|
|
298af217e9 | ||
|
|
89322c4b03 | ||
|
|
7d64f6a7dd | ||
|
|
b7efc92099 | ||
|
|
bc8b6284a6 | ||
|
|
20a187fab9 | ||
|
|
0b001f41c0 | ||
|
|
242977c7a5 | ||
|
|
364ac331da | ||
|
|
fcc0a80f4e | ||
|
|
8996f35cc0 | ||
|
|
ed5eb5c6db | ||
|
|
980309ae2a | ||
|
|
6a6bd4541a | ||
|
|
a14aa461ca |
@@ -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
|
||||
|
||||
|
||||
10
.idea/runConfigurations.xml
generated
10
.idea/runConfigurations.xml
generated
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:14.18.1-alpine
|
||||
FROM node:16.13.1-alpine
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
@@ -16,7 +16,7 @@ RUN set -x \
|
||||
make \
|
||||
nasm \
|
||||
libpng-dev \
|
||||
python \
|
||||
python3 \
|
||||
&& npm install --production \
|
||||
&& apk del .build-dependencies
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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;
|
||||
23
db/migrations/0187__add_changeId_to_entity_changes.sql
Normal file
23
db/migrations/0187__add_changeId_to_entity_changes.sql
Normal file
@@ -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`
|
||||
);
|
||||
@@ -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();
|
||||
|
||||
BIN
images/app-icons/png/96x96.png
Normal file
BIN
images/app-icons/png/96x96.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
2
libraries/autocomplete-theme-classic.css
Normal file
2
libraries/autocomplete-theme-classic.css
Normal file
File diff suppressed because one or more lines are too long
7
libraries/autocomplete.jquery.min.js
vendored
7
libraries/autocomplete.jquery.min.js
vendored
File diff suppressed because one or more lines are too long
9
libraries/autocomplete.js
Normal file
9
libraries/autocomplete.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
2
libraries/boxicons/css/boxicons.min.css
vendored
2
libraries/boxicons/css/boxicons.min.css
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
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.
3
libraries/commonmark.min.js
vendored
3
libraries/commonmark.min.js
vendored
File diff suppressed because one or more lines are too long
2
libraries/dayjs.min.js
vendored
2
libraries/dayjs.min.js
vendored
File diff suppressed because one or more lines are too long
6
libraries/force-graph.min.js
vendored
6
libraries/force-graph.min.js
vendored
File diff suppressed because one or more lines are too long
37
libraries/mermaid.min.js
vendored
37
libraries/mermaid.min.js
vendored
File diff suppressed because one or more lines are too long
2
libraries/normalize.min.css
vendored
Normal file
2
libraries/normalize.min.css
vendored
Normal file
@@ -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;
|
||||
}();
|
||||
2
libraries/wheel-zoom.min.js
vendored
2
libraries/wheel-zoom.min.js
vendored
File diff suppressed because one or more lines are too long
1087
package-lock.json
generated
1087
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.48.6",
|
||||
"version": "0.48.8",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
@@ -26,47 +26,48 @@
|
||||
"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",
|
||||
"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 +77,23 @@
|
||||
"tmp": "^0.2.1",
|
||||
"turndown": "7.1.1",
|
||||
"unescape": "1.0.1",
|
||||
"ws": "8.2.3",
|
||||
"ws": "8.3.0",
|
||||
"yauzl": "2.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "13.6.1",
|
||||
"electron-builder": "22.13.1",
|
||||
"electron": "16.0.4",
|
||||
"@electron/remote": "2.0.1",
|
||||
"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"
|
||||
|
||||
@@ -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,6 +58,9 @@ class Branch extends AbstractEntity {
|
||||
}
|
||||
|
||||
init() {
|
||||
this.becca.branches[this.branchId] = this;
|
||||
this.becca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
|
||||
|
||||
if (this.branchId === 'root') {
|
||||
return;
|
||||
}
|
||||
@@ -76,9 +79,6 @@ class Branch extends AbstractEntity {
|
||||
if (!parentNote.children.includes(childNote)) {
|
||||
parentNote.children.push(childNote);
|
||||
}
|
||||
|
||||
this.becca.branches[this.branchId] = this;
|
||||
this.becca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
|
||||
}
|
||||
|
||||
/** @returns {Note} */
|
||||
@@ -136,9 +136,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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,11 @@ bundleService.getWidgetBundlesByParent().then(widgetBundles => {
|
||||
|
||||
noteTooltipService.setupGlobalTooltip();
|
||||
|
||||
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;
|
||||
|
||||
@@ -41,7 +41,7 @@ export async function showDialog(widget, text = '') {
|
||||
$linkTitle.val(noteTitle);
|
||||
}
|
||||
|
||||
noteAutocompleteService.initNoteAutocomplete($autoComplete, {
|
||||
const ac = noteAutocompleteService.initNoteAutocomplete($autoComplete, {
|
||||
allowExternalLinks: true,
|
||||
allowCreatingNotes: true
|
||||
});
|
||||
@@ -84,7 +84,7 @@ export async function showDialog(widget, text = '') {
|
||||
});
|
||||
|
||||
if (text && text.trim()) {
|
||||
noteAutocompleteService.setText($autoComplete, text);
|
||||
noteAutocompleteService.setText(ac, text);
|
||||
}
|
||||
else {
|
||||
noteAutocompleteService.showRecentNotes($autoComplete);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -12,7 +12,12 @@ const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
|
||||
export async function showDialog() {
|
||||
utils.openDialog($dialog);
|
||||
|
||||
noteAutocompleteService.initNoteAutocomplete($autoComplete, { hideGoToSelectedNoteButton: true })
|
||||
const ac = noteAutocompleteService.initNoteAutocomplete($autoComplete, {
|
||||
hideGoToSelectedNoteButton: true,
|
||||
placeholder: "search for note by its name"
|
||||
});
|
||||
|
||||
$autoComplete
|
||||
// clear any event listener added in previous invocation of this function
|
||||
.off('autocomplete:noteselected')
|
||||
.on('autocomplete:noteselected', function(event, suggestion, dataset) {
|
||||
@@ -28,15 +33,12 @@ export async function showDialog() {
|
||||
// so we'll keep the content.
|
||||
// if it's outside of this time limit then we assume it's a completely new search and show recent notes instead.
|
||||
if (Date.now() - lastOpenedTs > KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000) {
|
||||
noteAutocompleteService.showRecentNotes($autoComplete);
|
||||
noteAutocompleteService.showRecentNotes(ac, $autoComplete);
|
||||
}
|
||||
else {
|
||||
$autoComplete
|
||||
// hack, the actual search value is stored in <pre> element next to the search input
|
||||
// this is important because the search input value is replaced with the suggestion note's title
|
||||
.autocomplete("val", $autoComplete.next().text())
|
||||
.trigger('focus')
|
||||
.trigger('select');
|
||||
ac.setIsOpen(true);
|
||||
ac.ext.focus();
|
||||
ac.ext.select();
|
||||
}
|
||||
|
||||
lastOpenedTs = Date.now();
|
||||
|
||||
@@ -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">
|
||||
<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 (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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -67,7 +81,7 @@ const TPL = `
|
||||
|
||||
<div class="form-group">
|
||||
<label for="protected-session-timeout-in-seconds">Protected session timeout (in seconds)</label>
|
||||
<input class="form-control" id="protected-session-timeout-in-seconds" type="number">
|
||||
<input class="form-control" id="protected-session-timeout-in-seconds" type="number" min="60">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +92,7 @@ const TPL = `
|
||||
|
||||
<div class="form-group">
|
||||
<label for="note-revision-snapshot-time-interval-in-seconds">Note revision snapshot time interval (in seconds)</label>
|
||||
<input class="form-control" id="note-revision-snapshot-time-interval-in-seconds" type="number">
|
||||
<input class="form-control" id="note-revision-snapshot-time-interval-in-seconds" type="number" min="10">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -89,12 +103,12 @@ const TPL = `
|
||||
|
||||
<div class="form-group">
|
||||
<label for="auto-readonly-size-text">Automatic readonly size (text notes)</label>
|
||||
<input class="form-control" id="auto-readonly-size-text" type="number">
|
||||
<input class="form-control" id="auto-readonly-size-text" type="number" min="0">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="auto-readonly-size-code">Automatic readonly size (code notes)</label>
|
||||
<input class="form-control" id="auto-readonly-size-code" type="number">
|
||||
<input class="form-control" id="auto-readonly-size-code" type="number" min="0">
|
||||
</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;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ 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";
|
||||
|
||||
export default class DesktopLayout {
|
||||
constructor(customWidgets) {
|
||||
@@ -110,9 +112,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")
|
||||
@@ -148,6 +148,7 @@ export default class DesktopLayout {
|
||||
.button(new NoteActionsWidget())
|
||||
)
|
||||
.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;
|
||||
@@ -88,13 +95,14 @@ export default class MobileLayout {
|
||||
return new FlexContainer('row').cssBlock(MOBILE_CSS)
|
||||
.setParent(appContext)
|
||||
.id('root-widget')
|
||||
.css('height', '100vh')
|
||||
.css('height', '100%')
|
||||
.child(new ScreenContainer("tree", 'column')
|
||||
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-5 col-md-4 col-lg-4 col-xl-4")
|
||||
.css("max-height", "100%")
|
||||
.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")
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
@@ -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) {
|
||||
|
||||
@@ -8,7 +8,75 @@ import froca from "./froca.js";
|
||||
// this key needs to have this value so it's hit by the tooltip
|
||||
const SELECTED_NOTE_PATH_KEY = "data-note-path";
|
||||
|
||||
const SELECTED_EXTERNAL_LINK_KEY = "data-external-link";
|
||||
const acMixin = {
|
||||
selectedNotePath: "",
|
||||
selectedExternalLink: "",
|
||||
$el: "",
|
||||
|
||||
focus() {
|
||||
this.$el.find('.aa-Input').focus();
|
||||
},
|
||||
|
||||
select() {
|
||||
this.$el.find('.aa-Input').select();
|
||||
},
|
||||
|
||||
getQuery() {
|
||||
return this?.lastState.query;
|
||||
},
|
||||
|
||||
getSelectedNotePath() {
|
||||
if (!this.getQuery()) {
|
||||
return "";
|
||||
} else {
|
||||
return this.selectedNotePath;
|
||||
}
|
||||
},
|
||||
|
||||
getSelectedNoteId() {
|
||||
const notePath = this.getSelectedNotePath();
|
||||
const chunks = notePath.split('/');
|
||||
|
||||
return chunks.length >= 1 ? chunks[chunks.length - 1] : null;
|
||||
},
|
||||
|
||||
setSelectedNotePath(notePath) {
|
||||
notePath = notePath || "";
|
||||
|
||||
this.selectedNotePath = notePath;
|
||||
|
||||
$(this)
|
||||
.closest(".input-group")
|
||||
.find(".go-to-selected-note-button")
|
||||
.toggleClass("disabled", !notePath.trim())
|
||||
.attr(SELECTED_NOTE_PATH_KEY, notePath); // we also set attr here so tooltip can be displayed
|
||||
},
|
||||
|
||||
getSelectedExternalLink() {
|
||||
if (!$(this).val().trim()) {
|
||||
return "";
|
||||
} else {
|
||||
return this.selectedExternalLink;
|
||||
}
|
||||
},
|
||||
|
||||
setSelectedExternalLink(externalLink) {
|
||||
this.selectedExternalLink = externalLink;
|
||||
|
||||
$(this)
|
||||
.closest(".input-group")
|
||||
.find(".go-to-selected-note-button")
|
||||
.toggleClass("disabled", true);
|
||||
},
|
||||
|
||||
async setNote(noteId) {
|
||||
const note = noteId ? await froca.getNote(noteId, true) : null;
|
||||
|
||||
$(this)
|
||||
.val(note ? note.title : "")
|
||||
.setSelectedNotePath(noteId);
|
||||
}
|
||||
}
|
||||
|
||||
async function autocompleteSourceForCKEditor(queryText) {
|
||||
return await new Promise((res, rej) => {
|
||||
@@ -30,7 +98,7 @@ async function autocompleteSourceForCKEditor(queryText) {
|
||||
});
|
||||
}
|
||||
|
||||
async function autocompleteSource(term, cb, options = {}) {
|
||||
async function autocompleteSource(term, options = {}) {
|
||||
const activeNoteId = appContext.tabManager.getActiveContextNoteId();
|
||||
|
||||
let results = await server.get('autocomplete'
|
||||
@@ -58,206 +126,176 @@ async function autocompleteSource(term, cb, options = {}) {
|
||||
].concat(results);
|
||||
}
|
||||
|
||||
cb(results);
|
||||
return results;
|
||||
}
|
||||
|
||||
function clearText($el) {
|
||||
function clearText(acObj) {
|
||||
if (utils.isMobile()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$el.setSelectedNotePath("");
|
||||
$el.autocomplete("val", "").trigger('change');
|
||||
acObj.ext.setSelectedNotePath("");
|
||||
acObj.setQuery("");
|
||||
}
|
||||
|
||||
function setText($el, text) {
|
||||
function setText(ac, text) {
|
||||
if (utils.isMobile()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$el.setSelectedNotePath("");
|
||||
$el
|
||||
.autocomplete("val", text.trim())
|
||||
.autocomplete("open");
|
||||
ac.ext.setSelectedNotePath("");
|
||||
ac.setQuery(text.trim());
|
||||
ac.setIsOpen(true);
|
||||
}
|
||||
|
||||
function showRecentNotes($el) {
|
||||
function showRecentNotes(ac) {
|
||||
if (utils.isMobile()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$el.setSelectedNotePath("");
|
||||
$el.autocomplete("val", "");
|
||||
$el.trigger('focus');
|
||||
ac.ext.$el.find(".aa-Input").val("").change();
|
||||
|
||||
ac.setQuery("");
|
||||
ac.setIsOpen(true);
|
||||
ac.update();
|
||||
ac.ext.setSelectedNotePath("");
|
||||
ac.ext.focus();
|
||||
|
||||
|
||||
console.log("BBB");
|
||||
}
|
||||
|
||||
function initNoteAutocomplete($el, options) {
|
||||
if ($el.hasClass("note-autocomplete-input") || utils.isMobile()) {
|
||||
function initNoteAutocomplete($container, options) {
|
||||
if ($container.hasClass("note-autocomplete-container") || utils.isMobile()) {
|
||||
// clear any event listener added in previous invocation of this function
|
||||
$el.off('autocomplete:noteselected');
|
||||
$container.off('autocomplete:noteselected');
|
||||
|
||||
return $el;
|
||||
return $container.prop("acObj");
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
$el.addClass("note-autocomplete-input");
|
||||
const $el = $('<div class="note-autocomplete-input">');
|
||||
const $sideButtons = $('<div>');
|
||||
|
||||
const $clearTextButton = $("<a>")
|
||||
.addClass("input-group-text input-clearer-button bx bx-x")
|
||||
.prop("title", "Clear text field");
|
||||
$container.addClass("note-autocomplete-container")
|
||||
.append($el)
|
||||
.append($sideButtons);
|
||||
|
||||
const $showRecentNotesButton = $("<a>")
|
||||
.addClass("input-group-text show-recent-notes-button bx bx-time")
|
||||
.addClass("show-recent-notes-button bx bx-time")
|
||||
.prop("title", "Show recent notes");
|
||||
|
||||
const $goToSelectedNoteButton = $("<a>")
|
||||
.addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right")
|
||||
.addClass("go-to-selected-note-button bx bx-arrow-to-right")
|
||||
.attr("data-action", "note");
|
||||
|
||||
const $sideButtons = $("<div>")
|
||||
.addClass("input-group-append")
|
||||
.append($clearTextButton)
|
||||
.append($showRecentNotesButton);
|
||||
$sideButtons.append($showRecentNotesButton);
|
||||
|
||||
if (!options.hideGoToSelectedNoteButton) {
|
||||
$sideButtons.append($goToSelectedNoteButton);
|
||||
}
|
||||
|
||||
$el.after($sideButtons);
|
||||
|
||||
$clearTextButton.on('click', () => clearText($el));
|
||||
|
||||
$showRecentNotesButton.on('click', e => {
|
||||
showRecentNotes($el);
|
||||
showRecentNotes(acObj);
|
||||
|
||||
// this will cause the click not give focus to the "show recent notes" button
|
||||
// this is important because otherwise input will lose focus immediatelly and not show the results
|
||||
return false;
|
||||
});
|
||||
|
||||
$el.autocomplete({
|
||||
appendTo: document.querySelector('body'),
|
||||
hint: false,
|
||||
autoselect: true,
|
||||
const { autocomplete } = window['@algolia/autocomplete-js'];
|
||||
|
||||
let acObj = autocomplete({
|
||||
container: $el[0],
|
||||
defaultActiveItemId: 0,
|
||||
openOnFocus: true,
|
||||
minLength: 0,
|
||||
tabAutocomplete: false
|
||||
}, [
|
||||
{
|
||||
source: (term, cb) => autocompleteSource(term, cb, options),
|
||||
displayKey: 'notePathTitle',
|
||||
templates: {
|
||||
suggestion: suggestion => suggestion.highlightedNotePathTitle
|
||||
},
|
||||
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
|
||||
cache: false
|
||||
tabAutocomplete: false,
|
||||
placeholder: options.placeholder,
|
||||
onStateChange({ state }) {
|
||||
acObj.lastState = state;
|
||||
},
|
||||
async getSources({ query }) {
|
||||
const items = await autocompleteSource(query, options);
|
||||
|
||||
return [
|
||||
{
|
||||
getItems() {
|
||||
return items;
|
||||
},
|
||||
onSelect({item}) {
|
||||
acObj.ext.$el.trigger("autocomplete:selected", [item]);
|
||||
},
|
||||
displayKey: 'notePathTitle',
|
||||
templates: {
|
||||
item({ item, createElement }) {
|
||||
return createElement('div', {
|
||||
dangerouslySetInnerHTML: {
|
||||
__html: item.highlightedNotePathTitle,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
|
||||
cache: false
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
$el.on('autocomplete:selected', async (event, suggestion) => {
|
||||
if (suggestion.action === 'external-link') {
|
||||
$el.setSelectedNotePath(null);
|
||||
$el.setSelectedExternalLink(suggestion.externalLink);
|
||||
acObj.ext = {...acMixin, $el, $container };
|
||||
|
||||
$el.autocomplete("val", suggestion.externalLink);
|
||||
$container.prop("acObj", acObj);
|
||||
|
||||
$el.autocomplete("close");
|
||||
$container.on('autocomplete:selected', async (event, item) => {
|
||||
if (item.action === 'external-link') {
|
||||
acObj.ext.setSelectedNotePath(null);
|
||||
acObj.ext.setSelectedExternalLink(item.externalLink);
|
||||
|
||||
$el.trigger('autocomplete:externallinkselected', [suggestion]);
|
||||
acObj.setQuery(item.externalLink);
|
||||
|
||||
$container.autocomplete("close");
|
||||
|
||||
$container.trigger('autocomplete:externallinkselected', [item]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (suggestion.action === 'create-note') {
|
||||
const {note} = await noteCreateService.createNote(suggestion.parentNoteId, {
|
||||
title: suggestion.noteTitle,
|
||||
if (item.action === 'create-note') {
|
||||
const {note} = await noteCreateService.createNote(item.parentNoteId, {
|
||||
title: item.noteTitle,
|
||||
activate: false
|
||||
});
|
||||
|
||||
suggestion.notePath = treeService.getSomeNotePath(note);
|
||||
item.notePath = treeService.getSomeNotePath(note);
|
||||
}
|
||||
|
||||
$el.setSelectedNotePath(suggestion.notePath);
|
||||
$el.setSelectedExternalLink(null);
|
||||
acObj.ext.setSelectedNotePath(item.notePath);
|
||||
acObj.ext.setSelectedExternalLink(null);
|
||||
|
||||
$el.autocomplete("val", suggestion.noteTitle);
|
||||
acObj.setQuery(item.noteTitle);
|
||||
|
||||
$el.autocomplete("close");
|
||||
acObj.setIsOpen(false);
|
||||
|
||||
$el.trigger('autocomplete:noteselected', [suggestion]);
|
||||
$container.trigger('autocomplete:noteselected', [item]);
|
||||
});
|
||||
|
||||
$el.on('autocomplete:closed', () => {
|
||||
if (!$el.val().trim()) {
|
||||
clearText($el);
|
||||
$container.on('autocomplete:closed', () => {
|
||||
if (!$container.val().trim()) {
|
||||
clearText($container);
|
||||
}
|
||||
});
|
||||
|
||||
$el.on('autocomplete:opened', () => {
|
||||
if ($el.attr("readonly")) {
|
||||
$el.autocomplete('close');
|
||||
$container.on('autocomplete:opened', () => {
|
||||
if ($container.attr("readonly")) {
|
||||
$container.autocomplete('close');
|
||||
}
|
||||
});
|
||||
|
||||
// clear any event listener added in previous invocation of this function
|
||||
$el.off('autocomplete:noteselected');
|
||||
$container.off('autocomplete:noteselected');
|
||||
|
||||
return $el;
|
||||
}
|
||||
|
||||
function init() {
|
||||
$.fn.getSelectedNotePath = function () {
|
||||
if (!$(this).val().trim()) {
|
||||
return "";
|
||||
} else {
|
||||
return $(this).attr(SELECTED_NOTE_PATH_KEY);
|
||||
}
|
||||
};
|
||||
|
||||
$.fn.getSelectedNoteId = function () {
|
||||
const notePath = $(this).getSelectedNotePath();
|
||||
const chunks = notePath.split('/');
|
||||
|
||||
return chunks.length >= 1 ? chunks[chunks.length - 1] : null;
|
||||
}
|
||||
|
||||
$.fn.setSelectedNotePath = function (notePath) {
|
||||
notePath = notePath || "";
|
||||
|
||||
$(this).attr(SELECTED_NOTE_PATH_KEY, notePath);
|
||||
|
||||
$(this)
|
||||
.closest(".input-group")
|
||||
.find(".go-to-selected-note-button")
|
||||
.toggleClass("disabled", !notePath.trim())
|
||||
.attr(SELECTED_NOTE_PATH_KEY, notePath); // we also set attr here so tooltip can be displayed
|
||||
};
|
||||
|
||||
$.fn.getSelectedExternalLink = function () {
|
||||
if (!$(this).val().trim()) {
|
||||
return "";
|
||||
} else {
|
||||
return $(this).attr(SELECTED_EXTERNAL_LINK_KEY);
|
||||
}
|
||||
};
|
||||
|
||||
$.fn.setSelectedExternalLink = function (externalLink) {
|
||||
$(this).attr(SELECTED_EXTERNAL_LINK_KEY, externalLink);
|
||||
|
||||
$(this)
|
||||
.closest(".input-group")
|
||||
.find(".go-to-selected-note-button")
|
||||
.toggleClass("disabled", true);
|
||||
}
|
||||
|
||||
$.fn.setNote = async function (noteId) {
|
||||
const note = noteId ? await froca.getNote(noteId, true) : null;
|
||||
|
||||
$(this)
|
||||
.val(note ? note.title : "")
|
||||
.setSelectedNotePath(noteId);
|
||||
}
|
||||
return acObj;
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -265,6 +303,5 @@ export default {
|
||||
autocompleteSourceForCKEditor,
|
||||
initNoteAutocomplete,
|
||||
showRecentNotes,
|
||||
setText,
|
||||
init
|
||||
setText
|
||||
}
|
||||
|
||||
@@ -59,10 +59,10 @@ class NoteContext extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
if (utils.isDesktop()) {
|
||||
// close dangling autocompletes after closing the tab
|
||||
$(".aa-input").autocomplete("close");
|
||||
}
|
||||
// if (utils.isDesktop()) {
|
||||
// // close dangling autocompletes after closing the tab
|
||||
// $(".aa-input").autocomplete("close");
|
||||
// }
|
||||
}
|
||||
|
||||
getSubContexts() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,9 +181,9 @@ async function consumeFrontendUpdateData() {
|
||||
|
||||
for (const entityChange of nonProcessedEntityChanges) {
|
||||
processedEntityChangeIds.add(entityChange.id);
|
||||
}
|
||||
|
||||
lastProcessedEntityChangeId = Math.max(lastProcessedEntityChangeId, allEntityChanges[allEntityChanges.length - 1].id);
|
||||
lastProcessedEntityChangeId = Math.max(lastProcessedEntityChangeId, entityChange.id);
|
||||
}
|
||||
}
|
||||
|
||||
checkEntityChangeIdListeners();
|
||||
|
||||
@@ -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,8 @@ 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)"
|
||||
},
|
||||
"relation": {
|
||||
"runOnNoteCreation": "executes when note is created on backend",
|
||||
@@ -289,7 +292,9 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
this.$rowTargetNote = this.$widget.find('.attr-row-target-note');
|
||||
this.$inputTargetNote = this.$widget.find('.attr-input-target-note');
|
||||
|
||||
noteAutocompleteService.initNoteAutocomplete(this.$inputTargetNote, {allowCreatingNotes: true})
|
||||
noteAutocompleteService.initNoteAutocomplete(this.$inputTargetNote, {allowCreatingNotes: true});
|
||||
|
||||
this.$inputTargetNote
|
||||
.on('autocomplete:noteselected', (event, suggestion, dataset) => {
|
||||
if (!suggestion.notePath) {
|
||||
return false;
|
||||
|
||||
157
src/public/app/widgets/backlinks.js
Normal file
157
src/public/app/widgets/backlinks.js
Normal file
@@ -0,0 +1,157 @@
|
||||
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-ticker:hover {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
.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();
|
||||
|
||||
const targetRelationCount = note.getTargetRelations().length;
|
||||
if (targetRelationCount === 0) {
|
||||
this.$ticker.hide();
|
||||
}
|
||||
else {
|
||||
this.$ticker.show();
|
||||
this.$count.text(
|
||||
`${targetRelationCount} backlink`
|
||||
+ (targetRelationCount === 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) {
|
||||
this.$items.append(await linkService.createNoteLink(backlink.noteId, {
|
||||
showNoteIcon: true,
|
||||
showNotePath: true,
|
||||
showTooltip: false
|
||||
}));
|
||||
|
||||
if (backlink.relationName) {
|
||||
this.$items.append($("<p>").text("relation: " + backlink.relationName));
|
||||
}
|
||||
else {
|
||||
this.$items.append(...backlink.excerpts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,18 @@ import froca from "../../services/froca.js";
|
||||
export default class OpenNoteButtonWidget extends ButtonWidget {
|
||||
targetNote(noteId) {
|
||||
froca.getNote(noteId).then(note => {
|
||||
if (!note) {
|
||||
console.log(`Note ${noteId} has not been found. This might happen on the first run before the target note is created.`);
|
||||
|
||||
if (!this.retried) {
|
||||
this.retried = true;
|
||||
|
||||
setTimeout(() => this.targetNote(noteId), 15000); // should be higher than timeout for createMissingSpecialNotes
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.icon(note.getIcon());
|
||||
this.title(note.title);
|
||||
|
||||
|
||||
38
src/public/app/widgets/buttons/update_available.js
Normal file
38
src/public/app/widgets/buttons/update_available.js
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export default class RootContainer extends FlexContainer {
|
||||
super('row');
|
||||
|
||||
this.id('root-widget');
|
||||
this.css('height', '100vh');
|
||||
this.css('height', '100%');
|
||||
}
|
||||
|
||||
refresh() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -150,6 +150,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$tree = this.$widget.find('.tree');
|
||||
this.$treeActions = this.$widget.find(".tree-actions");
|
||||
|
||||
this.$tree.on("mousedown", ".unhoist-button", () => hoistedNoteService.unhoist());
|
||||
this.$tree.on("mousedown", ".refresh-search-button", e => this.refreshSearch(e));
|
||||
@@ -200,20 +201,16 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
this.$hideIncludedImages.prop("checked", this.hideIncludedImages);
|
||||
this.$autoCollapseNoteTree.prop("checked", this.autoCollapseNoteTree);
|
||||
|
||||
let top = this.$treeSettingsButton[0].offsetTop;
|
||||
let left = this.$treeSettingsButton[0].offsetLeft;
|
||||
top -= this.$treeSettingsPopup.outerHeight() + 10;
|
||||
left += this.$treeSettingsButton.outerWidth() - this.$treeSettingsPopup.outerWidth();
|
||||
|
||||
if (left < 0) {
|
||||
left = 0;
|
||||
}
|
||||
const top = this.$treeActions[0].offsetTop - (this.$treeSettingsPopup.outerHeight());
|
||||
const left = Math.max(
|
||||
0,
|
||||
this.$treeActions[0].offsetLeft - this.$treeSettingsPopup.outerWidth() + this.$treeActions.outerWidth()
|
||||
);
|
||||
|
||||
this.$treeSettingsPopup.css({
|
||||
display: "block",
|
||||
top: top,
|
||||
left: left
|
||||
}).addClass("show");
|
||||
top,
|
||||
left
|
||||
}).show();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
|
||||
39
src/public/app/widgets/note_wrapper.js
Normal file
39
src/public/app/widgets/note_wrapper.js
Normal file
@@ -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')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -134,6 +148,8 @@ export default class QuickSearchWidget extends BasicWidget {
|
||||
}
|
||||
|
||||
async showInFullSearch() {
|
||||
this.$dropdownToggle.dropdown("hide");
|
||||
|
||||
const searchNote = await dateNotesService.createSearchNote({searchString: this.$searchString.val()});
|
||||
|
||||
await froca.loadSearchNote(searchNote.noteId);
|
||||
|
||||
@@ -69,7 +69,10 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
|
||||
this.$notePathList.empty();
|
||||
|
||||
if (this.noteId === 'root') {
|
||||
await this.getRenderedPath('root');
|
||||
this.$notePathList.empty().append(
|
||||
await this.getRenderedPath('root')
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -94,7 +97,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
|
||||
this.$notePathList.empty().append(...renderedPaths);
|
||||
}
|
||||
|
||||
async getRenderedPath(notePath, notePathRecord) {
|
||||
async getRenderedPath(notePath, notePathRecord = null) {
|
||||
const title = await treeService.getNotePathTitle(notePath);
|
||||
|
||||
const $noteLink = await linkService.createNoteLink(notePath, {title});
|
||||
@@ -109,20 +112,20 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
|
||||
$noteLink.addClass("path-current");
|
||||
}
|
||||
|
||||
if (notePathRecord.isInHoistedSubTree) {
|
||||
if (!notePathRecord || notePathRecord.isInHoistedSubTree) {
|
||||
$noteLink.addClass("path-in-hoisted-subtree");
|
||||
}
|
||||
else {
|
||||
icons.push(`<span class="bx bx-trending-up" title="This path is outside of hoisted note and you would have to unhoist."></span>`);
|
||||
}
|
||||
|
||||
if (notePathRecord.isArchived) {
|
||||
if (notePathRecord?.isArchived) {
|
||||
$noteLink.addClass("path-archived");
|
||||
|
||||
icons.push(`<span class="bx bx-archive" title="Archived"></span>`);
|
||||
}
|
||||
|
||||
if (notePathRecord.isSearch) {
|
||||
if (notePathRecord?.isSearch) {
|
||||
$noteLink.addClass("path-search");
|
||||
|
||||
icons.push(`<span class="bx bx-search" title="Search"></span>`);
|
||||
|
||||
@@ -115,9 +115,9 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
|
||||
const $input = $("<input>")
|
||||
.prop("tabindex", 200 + definitionAttr.position)
|
||||
.prop("attribute-id", valueAttr.noteId === this.noteId ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one
|
||||
.prop("attribute-type", valueAttr.type)
|
||||
.prop("attribute-name", valueAttr.name)
|
||||
.attr("data-attribute-id", valueAttr.noteId === this.noteId ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one
|
||||
.attr("data-attribute-type", valueAttr.type)
|
||||
.attr("data-attribute-name", valueAttr.name)
|
||||
.prop("value", valueAttr.value)
|
||||
.addClass("form-control")
|
||||
.addClass("promoted-attribute-input")
|
||||
@@ -230,7 +230,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
if (definition.multiplicity === "multi") {
|
||||
const addButton = $("<span>")
|
||||
const $addButton = $("<span>")
|
||||
.addClass("bx bx-plus pointer")
|
||||
.prop("title", "Add new attribute")
|
||||
.on('click', async () => {
|
||||
@@ -246,12 +246,28 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
$new.find('input').trigger('focus');
|
||||
});
|
||||
|
||||
const removeButton = $("<span>")
|
||||
const $removeButton = $("<span>")
|
||||
.addClass("bx bx-trash pointer")
|
||||
.prop("title", "Remove this attribute")
|
||||
.on('click', async () => {
|
||||
if (valueAttr.attributeId) {
|
||||
await server.remove("notes/" + this.noteId + "/attributes/" + valueAttr.attributeId, this.componentId);
|
||||
const attributeId = $input.attr("data-attribute-id");
|
||||
|
||||
if (attributeId) {
|
||||
await server.remove("notes/" + this.noteId + "/attributes/" + attributeId, this.componentId);
|
||||
}
|
||||
|
||||
// if it's the last one the create new empty form immediately
|
||||
const sameAttrSelector = `input[data-attribute-type='${valueAttr.type}'][data-attribute-name='${valueName}']`;
|
||||
|
||||
if (this.$widget.find(sameAttrSelector).length <= 1) {
|
||||
const $new = await this.createPromotedAttributeCell(definitionAttr, {
|
||||
attributeId: "",
|
||||
type: valueAttr.type,
|
||||
name: valueName,
|
||||
value: ""
|
||||
}, valueName);
|
||||
|
||||
$wrapper.after($new);
|
||||
}
|
||||
|
||||
$wrapper.remove();
|
||||
@@ -259,9 +275,9 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
|
||||
$multiplicityCell
|
||||
.append(" ")
|
||||
.append(addButton)
|
||||
.append($addButton)
|
||||
.append(" ")
|
||||
.append(removeButton);
|
||||
.append($removeButton);
|
||||
}
|
||||
|
||||
return $wrapper;
|
||||
@@ -275,7 +291,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
if ($attr.prop("type") === "checkbox") {
|
||||
value = $attr.is(':checked') ? "true" : "false";
|
||||
}
|
||||
else if ($attr.prop("attribute-type") === "relation") {
|
||||
else if ($attr.attr("data-attribute-type") === "relation") {
|
||||
const selectedPath = $attr.getSelectedNotePath();
|
||||
|
||||
value = selectedPath ? treeService.getNoteIdFromNotePath(selectedPath) : "";
|
||||
@@ -285,13 +301,13 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
const result = await server.put(`notes/${this.noteId}/attribute`, {
|
||||
attributeId: $attr.prop("attribute-id"),
|
||||
type: $attr.prop("attribute-type"),
|
||||
name: $attr.prop("attribute-name"),
|
||||
attributeId: $attr.attr("data-attribute-id"),
|
||||
type: $attr.attr("data-attribute-type"),
|
||||
name: $attr.attr("data-attribute-name"),
|
||||
value: value
|
||||
}, this.componentId);
|
||||
|
||||
$attr.prop("attribute-id", result.attributeId);
|
||||
$attr.attr("data-attribute-id", result.attributeId);
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,9 +57,7 @@ class ImageTypeWidget extends TypeWidget {
|
||||
}
|
||||
|
||||
async doRefresh(note) {
|
||||
const imageHash = utils.randomString(10);
|
||||
|
||||
this.$imageView.prop("src", `api/images/${note.noteId}/${note.title}?${imageHash}`);
|
||||
this.$imageView.prop("src", `api/images/${note.noteId}/${note.title}`);
|
||||
}
|
||||
|
||||
copyImageToClipboardEvent({ntxId}) {
|
||||
|
||||
87
src/public/stylesheets/share.css
Normal file
87
src/public/stylesheets/share.css
Normal file
@@ -0,0 +1,87 @@
|
||||
#layout {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#menu {
|
||||
padding: 20px;
|
||||
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: 10px 20px 0 20px;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.type-image img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: anywhere;
|
||||
}
|
||||
|
||||
#menuLink {
|
||||
position: fixed;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1.4em;
|
||||
background: #000;
|
||||
background: rgba(0,0,0,0.7);
|
||||
font-size: 2rem;
|
||||
z-index: 10;
|
||||
height: auto;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 48em) {
|
||||
#layout.active #menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#layout.active #main {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#layout.active #menuLink::after {
|
||||
content: "«";
|
||||
}
|
||||
|
||||
#menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#menuLink::after {
|
||||
content: "»";
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,11 @@ body {
|
||||
on the last line of the editor. */
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--main-background-color);
|
||||
color: var(--main-text-color);
|
||||
font-family: var(--main-font-family);
|
||||
font-size: var(--main-font-size);
|
||||
}
|
||||
|
||||
a, a:visited, a:hover {
|
||||
@@ -58,7 +60,7 @@ table td, table th {
|
||||
}
|
||||
|
||||
code, kbd, pre, samp {
|
||||
font-family: var(--monospace-font-family);
|
||||
font-family: var(--monospace-font-family) !important;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
@@ -356,9 +358,12 @@ pre:not(.CodeMirror-line) {
|
||||
color: var(--button-disabled-background-color) !important;
|
||||
}
|
||||
|
||||
.note-autocomplete-input {
|
||||
/* this is for seamless integration of "input clearer" button */
|
||||
border-right: 0;
|
||||
.note-autocomplete-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.note-autocomplete-container .note-autocomplete-input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
table.promoted-attributes-in-tooltip {
|
||||
@@ -430,43 +435,6 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.algolia-autocomplete {
|
||||
width: calc(100% - 30px);
|
||||
z-index: 2000 !important;
|
||||
}
|
||||
|
||||
.algolia-autocomplete .aa-input, .algolia-autocomplete .aa-hint {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.algolia-autocomplete .aa-dropdown-menu {
|
||||
width: 100%;
|
||||
background-color: var(--main-background-color);
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-top: none;
|
||||
z-index: 2000 !important;
|
||||
max-height: 500px;
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion {
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion.aa-cursor {
|
||||
color: var(--hover-item-text-color);
|
||||
background-color: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
.help-button {
|
||||
float: right;
|
||||
background: none;
|
||||
@@ -714,10 +682,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
||||
border-color: var(--main-border-color) !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: var(--main-font-size);
|
||||
}
|
||||
|
||||
.gutter {
|
||||
background: linear-gradient(to bottom, transparent, var(--accented-background-color), transparent);
|
||||
}
|
||||
@@ -956,3 +920,11 @@ input {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.note-split.full-content-width {
|
||||
max-width: 999999px;
|
||||
}
|
||||
|
||||
.aa-Panel {
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -60,6 +60,7 @@ function downloadNoteFile(noteId, res, contentDisposition = true) {
|
||||
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
|
||||
}
|
||||
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader('Content-Type', note.mime);
|
||||
|
||||
res.send(note.getContent());
|
||||
|
||||
@@ -20,6 +20,7 @@ function returnImage(req, res) {
|
||||
}
|
||||
|
||||
res.set('Content-Type', image.mime);
|
||||
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
|
||||
res.send(image.getContent());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const becca = require("../../becca/becca");
|
||||
const { JSDOM } = require("jsdom");
|
||||
|
||||
function buildDescendantCountMap() {
|
||||
const noteIdToCountMap = {};
|
||||
@@ -42,6 +43,11 @@ function getNeighbors(note, depth) {
|
||||
}
|
||||
|
||||
const targetNote = relation.getTargetNote();
|
||||
|
||||
if (targetNote.hasLabel('excludeFromNoteMap')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
retNoteIds.push(targetNote.noteId);
|
||||
|
||||
for (const noteId of getNeighbors(targetNote, depth - 1)) {
|
||||
@@ -56,6 +62,11 @@ function getNeighbors(note, depth) {
|
||||
}
|
||||
|
||||
const sourceNote = relation.getNote();
|
||||
|
||||
if (sourceNote.hasLabel('excludeFromNoteMap')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
retNoteIds.push(sourceNote.noteId);
|
||||
|
||||
for (const noteId of getNeighbors(sourceNote, depth - 1)) {
|
||||
@@ -174,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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -203,6 +202,10 @@ function changeTitle(req) {
|
||||
|
||||
const noteTitleChanged = note.title !== title;
|
||||
|
||||
if (noteTitleChanged) {
|
||||
noteService.saveNoteRevision(note);
|
||||
}
|
||||
|
||||
note.title = title;
|
||||
|
||||
note.save();
|
||||
|
||||
@@ -54,7 +54,8 @@ const ALLOWED_OPTIONS = new Set([
|
||||
'dailyBackupEnabled',
|
||||
'weeklyBackupEnabled',
|
||||
'monthlyBackupEnabled',
|
||||
'maxContentWidth'
|
||||
'maxContentWidth',
|
||||
'compressImages'
|
||||
]);
|
||||
|
||||
function getOptions() {
|
||||
|
||||
@@ -204,6 +204,11 @@ function queueSector(req) {
|
||||
entityChangesService.addEntityChangesForSector(entityName, sector);
|
||||
}
|
||||
|
||||
function checkEntityChanges() {
|
||||
const consistencyChecks = require("../../services/consistency_checks");
|
||||
consistencyChecks.runEntityChangesChecks();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
testSync,
|
||||
checkSync,
|
||||
@@ -215,5 +220,6 @@ module.exports = {
|
||||
update,
|
||||
getStats,
|
||||
syncFinished,
|
||||
queueSector
|
||||
queueSector,
|
||||
checkEntityChanges
|
||||
};
|
||||
|
||||
@@ -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,7 +213,7 @@ 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);
|
||||
@@ -259,6 +260,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);
|
||||
@@ -294,6 +296,7 @@ function register(app) {
|
||||
route(GET, '/api/sync/changed', [auth.checkApiAuth], syncApiRoute.getChanged, apiResultHandler);
|
||||
route(PUT, '/api/sync/update', [auth.checkApiAuth], syncApiRoute.update, apiResultHandler);
|
||||
route(POST, '/api/sync/finished', [auth.checkApiAuth], syncApiRoute.syncFinished, apiResultHandler);
|
||||
route(POST, '/api/sync/check-entity-changes', [auth.checkApiAuth], syncApiRoute.checkEntityChanges, apiResultHandler);
|
||||
route(POST, '/api/sync/queue-sector/:entityName/:sector', [auth.checkApiAuth], syncApiRoute.queueSector, apiResultHandler);
|
||||
route(GET, '/api/sync/stats', [], syncApiRoute.getStats, apiResultHandler);
|
||||
|
||||
@@ -366,6 +369,8 @@ function register(app) {
|
||||
|
||||
route(GET, '/api/fonts', [auth.checkApiAuthOrElectron], fontsRoute.getFontCss);
|
||||
|
||||
shareRoutes.register(router);
|
||||
|
||||
app.use('', router);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ const build = require('./build');
|
||||
const packageJson = require('../../package');
|
||||
const {TRILIUM_DATA_DIR} = require('./data_dir');
|
||||
|
||||
const APP_DB_VERSION = 185;
|
||||
const SYNC_VERSION = 21;
|
||||
const APP_DB_VERSION = 187;
|
||||
const SYNC_VERSION = 23;
|
||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -49,6 +49,9 @@ const BUILTIN_ATTRIBUTES = [
|
||||
{ type: 'label', name: 'mapRootNoteId' },
|
||||
{ type: 'label', name: 'bookmarked' },
|
||||
{ type: 'label', name: 'bookmarkFolder' },
|
||||
{ type: 'label', name: 'sorted' },
|
||||
{ type: 'label', name: 'top' },
|
||||
{ type: 'label', name: 'fullContentWidth' },
|
||||
|
||||
// relation names
|
||||
{ type: 'relation', name: 'runOnNoteCreation', isDangerous: true },
|
||||
@@ -62,6 +65,7 @@ const BUILTIN_ATTRIBUTES = [
|
||||
{ type: 'relation', name: 'renderNote', isDangerous: true }
|
||||
];
|
||||
|
||||
/** @returns {Note[]} */
|
||||
function getNotesWithLabel(name, value) {
|
||||
const query = formatAttrForSearch({type: 'label', name, value}, true);
|
||||
return searchService.searchNotes(query, {
|
||||
@@ -71,6 +75,7 @@ function getNotesWithLabel(name, value) {
|
||||
}
|
||||
|
||||
// TODO: should be in search service
|
||||
/** @returns {Note|null} */
|
||||
function getNoteWithLabel(name, value) {
|
||||
// optimized version (~20 times faster) without using normal search, useful for e.g. finding date notes
|
||||
const attrs = becca.findAttributes('label', name);
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2021-11-13T22:49:58+01:00", buildRevision: "c94603010630cfafe64575ab378c482bb39fb083" };
|
||||
module.exports = { buildDate:"2021-12-13T11:12:31+01:00", buildRevision: "d9550dd59b9b0dff0b229c400cdf6585abcb226a" };
|
||||
|
||||
@@ -44,14 +44,10 @@ function isEntityEventsDisabled() {
|
||||
return !!namespace.get('disableEntityEvents');
|
||||
}
|
||||
|
||||
function clearEntityChangeIds() {
|
||||
namespace.set('entityChangeIds', []);
|
||||
}
|
||||
|
||||
function getAndClearEntityChangeIds() {
|
||||
const entityChangeIds = namespace.get('entityChangeIds') || [];
|
||||
|
||||
clearEntityChangeIds();
|
||||
namespace.set('entityChangeIds', []);
|
||||
|
||||
return entityChangeIds;
|
||||
}
|
||||
@@ -89,7 +85,6 @@ module.exports = {
|
||||
disableEntityEvents,
|
||||
isEntityEventsDisabled,
|
||||
reset,
|
||||
clearEntityChangeIds,
|
||||
getAndClearEntityChangeIds,
|
||||
addEntityChange,
|
||||
ignoreEntityChangeIds
|
||||
|
||||
@@ -701,6 +701,11 @@ function runOnDemandChecks(autoFix) {
|
||||
consistencyChecks.runChecks();
|
||||
}
|
||||
|
||||
function runEntityChangesChecks() {
|
||||
const consistencyChecks = new ConsistencyChecks(true);
|
||||
consistencyChecks.findEntityChangeIssues();
|
||||
}
|
||||
|
||||
sqlInit.dbReady.then(() => {
|
||||
setInterval(cls.wrap(runPeriodicChecks), 60 * 60 * 1000);
|
||||
|
||||
@@ -709,5 +714,6 @@ sqlInit.dbReady.then(() => {
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
runOnDemandChecks
|
||||
runOnDemandChecks,
|
||||
runEntityChangesChecks
|
||||
};
|
||||
|
||||
@@ -25,15 +25,6 @@ function createNote(parentNote, noteTitle) {
|
||||
}).note;
|
||||
}
|
||||
|
||||
function getNoteStartingWith(parentNoteId, startsWith) {
|
||||
const noteId = sql.getValue(`SELECT notes.noteId FROM notes JOIN branches USING(noteId)
|
||||
WHERE parentNoteId = ? AND title LIKE '${startsWith}%'
|
||||
AND notes.isDeleted = 0 AND isProtected = 0
|
||||
AND branches.isDeleted = 0`, [parentNoteId]);
|
||||
|
||||
return becca.getNote(noteId);
|
||||
}
|
||||
|
||||
/** @returns {Note} */
|
||||
function getRootCalendarNote() {
|
||||
let rootNote = attributeService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
|
||||
@@ -65,8 +56,7 @@ function getYearNote(dateStr, rootNote) {
|
||||
|
||||
const yearStr = dateStr.substr(0, 4);
|
||||
|
||||
let yearNote = attributeService.getNoteWithLabel(YEAR_LABEL, yearStr)
|
||||
|| getNoteStartingWith(rootNote.noteId, yearStr);
|
||||
let yearNote = attributeService.getNoteWithLabel(YEAR_LABEL, yearStr);
|
||||
|
||||
if (yearNote) {
|
||||
return yearNote;
|
||||
@@ -112,18 +102,12 @@ function getMonthNote(dateStr, rootNote) {
|
||||
return monthNote;
|
||||
}
|
||||
|
||||
const yearNote = getYearNote(dateStr, rootNote);
|
||||
|
||||
monthNote = getNoteStartingWith(yearNote.noteId, monthNumber);
|
||||
|
||||
if (monthNote) {
|
||||
return monthNote;
|
||||
}
|
||||
|
||||
const dateObj = dateUtils.parseLocalDate(dateStr);
|
||||
|
||||
const noteTitle = getMonthNoteTitle(rootNote, monthNumber, dateObj);
|
||||
|
||||
const yearNote = getYearNote(dateStr, rootNote);
|
||||
|
||||
sql.transactional(() => {
|
||||
monthNote = createNote(yearNote, noteTitle);
|
||||
|
||||
@@ -164,12 +148,6 @@ function getDateNote(dateStr) {
|
||||
const monthNote = getMonthNote(dateStr, rootNote);
|
||||
const dayNumber = dateStr.substr(8, 2);
|
||||
|
||||
dateNote = getNoteStartingWith(monthNote.noteId, dayNumber);
|
||||
|
||||
if (dateNote) {
|
||||
return dateNote;
|
||||
}
|
||||
|
||||
const dateObj = dateUtils.parseLocalDate(dateStr);
|
||||
|
||||
const noteTitle = getDateNoteTitle(rootNote, dayNumber, dateObj);
|
||||
|
||||
@@ -3,15 +3,18 @@ const sourceIdService = require('./source_id');
|
||||
const dateUtils = require('./date_utils');
|
||||
const log = require('./log');
|
||||
const cls = require('./cls');
|
||||
const utils = require('./utils');
|
||||
const becca = require("../becca/becca");
|
||||
|
||||
let maxEntityChangeId = 0;
|
||||
|
||||
function addEntityChange(origEntityChange, keepOriginalId = false) {
|
||||
function addEntityChange(origEntityChange) {
|
||||
const ec = {...origEntityChange};
|
||||
|
||||
if (!keepOriginalId) {
|
||||
delete ec.id;
|
||||
delete ec.id;
|
||||
|
||||
if (!ec.changeId) {
|
||||
ec.changeId = utils.randomString(12);
|
||||
}
|
||||
|
||||
ec.sourceId = ec.sourceId || cls.getSourceId() || sourceIdService.getCurrentSourceId();
|
||||
|
||||
@@ -39,6 +39,10 @@ eventService.subscribe(eventService.NOTE_TITLE_CHANGED, note => {
|
||||
eventService.subscribe([ eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED ], ({ entityName, entity }) => {
|
||||
if (entityName === 'attributes') {
|
||||
runAttachedRelations(entity.getNote(), 'runOnAttributeChange', entity);
|
||||
|
||||
if (entity.type === 'label' && entity.name === 'sorted') {
|
||||
handleSortedAttribute(entity);
|
||||
}
|
||||
}
|
||||
else if (entityName === 'notes') {
|
||||
runAttachedRelations(entity, 'runOnNoteChange', entity);
|
||||
@@ -83,17 +87,7 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
|
||||
}
|
||||
}
|
||||
else if (entity.type === 'label' && entity.name === 'sorted') {
|
||||
treeService.sortNotesIfNeeded(entity.noteId);
|
||||
|
||||
if (entity.isInheritable) {
|
||||
const note = becca.notes[entity.noteId];
|
||||
|
||||
if (note) {
|
||||
for (const noteId of note.getSubtreeNoteIds()) {
|
||||
treeService.sortNotesIfNeeded(noteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
handleSortedAttribute(entity);
|
||||
}
|
||||
}
|
||||
else if (entityName === 'notes') {
|
||||
@@ -122,6 +116,20 @@ function processInverseRelations(entityName, entity, handler) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleSortedAttribute(entity) {
|
||||
treeService.sortNotesIfNeeded(entity.noteId);
|
||||
|
||||
if (entity.isInheritable) {
|
||||
const note = becca.notes[entity.noteId];
|
||||
|
||||
if (note) {
|
||||
for (const noteId of note.getSubtreeNoteIds()) {
|
||||
treeService.sortNotesIfNeeded(noteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) => {
|
||||
processInverseRelations(entityName, entity, (definition, note, targetNote) => {
|
||||
// we need to make sure that also target's inverse attribute exists and if not, then create it
|
||||
|
||||
@@ -14,6 +14,7 @@ const isSvg = require('is-svg');
|
||||
const isAnimated = require('is-animated');
|
||||
|
||||
async function processImage(uploadBuffer, originalName, shrinkImageSwitch) {
|
||||
const compressImages = optionService.getOptionBool("compressImages");
|
||||
const origImageFormat = getImageType(uploadBuffer);
|
||||
|
||||
if (origImageFormat && ["webp", "svg", "gif"].includes(origImageFormat.ext)) {
|
||||
@@ -25,7 +26,7 @@ async function processImage(uploadBuffer, originalName, shrinkImageSwitch) {
|
||||
shrinkImageSwitch = false;
|
||||
}
|
||||
|
||||
const finalImageBuffer = shrinkImageSwitch ? await shrinkImage(uploadBuffer, originalName) : uploadBuffer;
|
||||
const finalImageBuffer = (compressImages && shrinkImageSwitch) ? await shrinkImage(uploadBuffer, originalName) : uploadBuffer;
|
||||
|
||||
const imageFormat = getImageType(finalImageBuffer);
|
||||
|
||||
|
||||
@@ -918,5 +918,6 @@ module.exports = {
|
||||
getUndeletedParentBranchIds,
|
||||
triggerNoteTitleChanged,
|
||||
eraseDeletedNotesNow,
|
||||
eraseNotesWithDeleteId
|
||||
eraseNotesWithDeleteId,
|
||||
saveNoteRevision
|
||||
};
|
||||
|
||||
@@ -91,6 +91,7 @@ const defaultOptions = [
|
||||
{ name: 'weeklyBackupEnabled', value: 'true', isSynced: false },
|
||||
{ name: 'monthlyBackupEnabled', value: 'true', isSynced: false },
|
||||
{ name: 'maxContentWidth', value: '1200', isSynced: false },
|
||||
{ name: 'compressImages', value: 'true', isSynced: true }
|
||||
];
|
||||
|
||||
function initStartupOptions() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user