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