Compare commits

...

117 Commits

Author SHA1 Message Date
zadam
ad8d35efe9 release 0.49.1-beta 2021-12-24 23:05:10 +01:00
zadam
0217b1c85d share pdf view is responsive 2021-12-24 22:46:55 +01:00
zadam
c0aa14f586 Merge remote-tracking branch 'origin/master' 2021-12-24 22:43:25 +01:00
zadam
b54cfab4ff share root itself is not shared, fixes #2468 2021-12-24 22:43:12 +01:00
Matt
a08985e7a6 Display PDF in shared notes (#2466)
* Add PDF rendering

* Cleanup
2021-12-24 22:36:31 +01:00
zadam
a789025025 don't show share switch for root and share root notes, #2465 2021-12-24 22:34:15 +01:00
zadam
3f307b117e fix webpack build 2021-12-24 22:18:05 +01:00
zadam
a232035d47 fix backlink count 2021-12-24 21:01:36 +01:00
zadam
9d38e9342d moved API docs button to the bottom of a code note 2021-12-24 20:40:27 +01:00
zadam
4bc4b9ade7 release 0.49.0-beta 2021-12-23 23:03:21 +01:00
zadam
f0217cae5e fix webpack build 2021-12-23 23:01:25 +01:00
zadam
da050c6369 release 0.49.0-beta 2021-12-23 22:33:49 +01:00
zadam
842c317568 cleanup 2021-12-23 21:08:43 +01:00
zadam
47845930f4 sharing improvements 2021-12-23 20:54:48 +01:00
zadam
972f2f40bf share improvements/cleanup 2021-12-23 12:54:21 +01:00
zadam
94111c464b changed symbol for shared notes 2021-12-22 16:02:36 +01:00
zadam
94e18dfb7c add symbol to shared notes in the tree 2021-12-22 15:01:54 +01:00
zadam
bc9903191e fix migration script 2021-12-22 13:14:50 +01:00
zadam
cd8c24ceae windows better-sqlite3 binary 2021-12-22 12:58:33 +01:00
zadam
f9709c9c39 added shareOmitDefaultCss label 2021-12-22 11:43:21 +01:00
zadam
c0964a4f12 added shareAlias label 2021-12-22 10:57:02 +01:00
zadam
bcef8579ce added shareCss relation and shareHiddenFromTree label 2021-12-22 09:36:38 +01:00
zadam
1180be75d1 added 404 error page 2021-12-22 09:10:38 +01:00
zadam
cfa49c7b1b feature request desc 2021-12-21 22:47:31 +01:00
zadam
8e4926ed7f cleanup of issue templates, closes #2461 2021-12-21 22:46:29 +01:00
zadam
2430dcba65 mac better_sqlite3.node 2021-12-21 22:02:25 +01:00
zadam
7c885a8b76 sharing WIP 2021-12-21 22:01:06 +01:00
zadam
402e29d6dc Merge remote-tracking branch 'origin/stable' 2021-12-21 16:13:09 +01:00
zadam
e7faebfac3 sharing WIP 2021-12-21 16:12:59 +01:00
zadam
10a5773c66 added consistency check to reconcile erased status between entity_changes and data 2021-12-21 14:25:26 +01:00
zadam
1e8472266f small fixes 2021-12-21 13:22:13 +01:00
zadam
b30792a3da make sure that the info about periodically erased entities is sent to the frontend 2021-12-21 11:04:41 +01:00
zadam
8b56fb10fd Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	package-lock.json
2021-12-20 22:24:50 +01:00
zadam
26602e8226 focus "jump to note" in new tab when there are no workspaces, #2455 2021-12-20 22:10:21 +01:00
zadam
b8eeb0371c fixed bug in search where note with inherited attribute is not matched when the owning note is not matched, #2457 2021-12-20 21:35:12 +01:00
zadam
b1c4737e78 fixed search tests 2021-12-20 21:31:44 +01:00
zadam
3860028a9e sharing WIP 2021-12-20 17:30:47 +01:00
zadam
16d97b95af optimized custom script/widget loading 2021-12-19 22:08:52 +01:00
zadam
20465a4f71 allow setting image labels in sender API 2021-12-19 21:56:19 +01:00
zadam
2ff6e50af4 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	src/public/app/dialogs/options/other.js
2021-12-19 10:54:30 +01:00
zadam
e0378c5064 optimized #keyboardShortcuts retrieval 2021-12-19 10:50:38 +01:00
zadam
e29aee1aae use lazy title loading in bookmarks buttons to have decrypted title upon protected session start, #2393 2021-12-19 10:36:48 +01:00
zadam
1aff42f453 fix setting exported file extension when truncated 2021-12-17 22:03:00 +01:00
zadam
a098630e09 add default JPG quality if value not in range 2021-12-16 22:10:51 +01:00
zadam
074eb1c02f importing of cloned notes should not depend on ZIP listing order, fixes #2440 2021-12-15 22:36:45 +01:00
zadam
657496ea37 Merge remote-tracking branch 'origin/master' 2021-12-14 21:55:00 +01:00
zadam
034aaa7209 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	src/public/app/dialogs/options/other.js
2021-12-14 21:54:38 +01:00
zadam
a81ea3771f release 0.48.8 2021-12-13 11:12:31 +01:00
Lake
4ceba8cc6e Correct "Options change have been saved." (#2436) 2021-12-12 21:08:54 +01:00
zadam
d9550dd59b fix "getNoteStartingWith" relic 2021-12-11 14:15:38 +01:00
zadam
97f7fe7b18 set note type/mime in PUT body to avoid http proxy slash reencoding, fixes #2419 2021-12-08 22:36:09 +01:00
zadam
a810c08c02 trigger note revisioning saving also on title changes #2426 2021-12-08 21:04:22 +01:00
zadam
ab550a1e8d share functionality WIP 2021-12-07 23:03:49 +01:00
zadam
08e8047d8a share functionality WIP 2021-12-06 22:53:17 +01:00
zadam
67da877135 Merge remote-tracking branch 'origin/stable' 2021-12-06 20:57:32 +01:00
zadam
263b7a84bb full text search should look into link URLs as well, closes #2412 2021-12-06 20:54:37 +01:00
zadam
9d18bebb13 "show in full search" closes the quick search dropdown 2021-12-06 20:43:50 +01:00
zadam
2d339dec6b share functionality WIP 2021-12-05 23:10:35 +01:00
zadam
b78ab1ee02 boxicons 2.1.1 2021-12-05 21:50:02 +01:00
Myzel394
2f5f116345 Improve tabs background coloring (#2395)
* improved tabs background coloring

* improvements

* reverted changes

* color improvements

* added light theme
2021-12-05 13:07:53 +01:00
zadam
64f1671566 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	package.json
2021-12-04 13:48:35 +01:00
zadam
26bcfe5160 fix hidden notes appearing in note map, closes #2403 2021-12-04 13:45:15 +01:00
zadam
89c04e6b6b fix "note paths" ribbon widget for root note 2021-12-04 13:33:31 +01:00
zadam
e079359c15 copied links from tree should be reference links 2021-12-04 12:50:02 +01:00
zadam
c4ab6b4866 copy action in the tree (or ctrl+c) will also save note links to the clipboard, fixes #2401 2021-12-04 12:45:27 +01:00
zadam
630d9f2e45 backlinks improvements, #2349 2021-12-02 22:00:42 +01:00
zadam
bbceb6251a backlinks WIP, #2349 2021-12-01 23:12:54 +01:00
zadam
89f117da5b Merge remote-tracking branch 'origin/stable' 2021-11-30 21:40:55 +01:00
zadam
40fb4ff56b fix for "Today page does not work for 2021-11-20", closes #2359 2021-11-30 21:21:16 +01:00
zadam
d64c14482b after removing last promoted attribute, create a blank one, fixes #2388 2021-11-26 23:39:08 +01:00
zadam
61f197dd81 Merge remote-tracking branch 'origin/stable' 2021-11-25 21:36:19 +01:00
zadam
564366861e prevent browser from caching images/files, fixes #2378 2021-11-25 20:24:42 +01:00
zadam
1ee2abcc42 keep some types (e.g. mermaid diagrams) of notes full width 2021-11-24 21:27:55 +01:00
zadam
f4242b4096 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	src/services/app_info.js
2021-11-24 20:08:34 +01:00
zadam
d59542dd6f Merge remote-tracking branch 'origin/master' 2021-11-23 23:09:37 +01:00
zadam
211ff90ee8 add "top" label to keep notes on top, allow sorting by label, #2343 2021-11-23 23:09:29 +01:00
zadam
8c11d022fb release 0.48.7 2021-11-23 21:53:32 +01:00
Myzel394
24210ef80c fixed settings menu (#2374)
(cherry picked from commit 3f40a52f65)
2021-11-23 21:38:13 +01:00
Myzel394
3f40a52f65 fixed settings menu (#2374) 2021-11-23 21:37:32 +01:00
zadam
df4cf80be4 fix version detection without running npm 2021-11-21 17:20:28 +01:00
zadam
bc854ee149 cleanup 2021-11-21 17:15:10 +01:00
Myzel394
b23ead8097 Fix: Highlighting searched term should ignore accents (#2364)
* fixed accent highlighting not working

* fixed

* fixes

* improvements
2021-11-21 16:27:50 +01:00
Myzel394
886fdf7cd6 Improve image compression (#2369)
* added options

* added checkbox handling to import into note

* added image compression option respecting
2021-11-21 16:27:13 +01:00
zadam
2135aa058e increase sync version to 22 2021-11-21 16:16:28 +01:00
zadam
de20183a22 Merge remote-tracking branch 'origin/stable' 2021-11-21 13:45:57 +01:00
zadam
42b5437c87 fix setup new document, closes #2368 2021-11-21 13:44:52 +01:00
zadam
67542f448d fix total height / scrolling on mobile chrome/safari, closes #2367 2021-11-21 13:39:47 +01:00
zadam
ae29c6bac4 global note map should respect hoisting, #2365 2021-11-21 10:40:48 +01:00
zadam
1dce96b4c1 Merge remote-tracking branch 'origin/stable' 2021-11-20 21:34:34 +01:00
zadam
08e9b59696 hide hidden subtree notes from search results, closes #2361 2021-11-20 21:01:37 +01:00
jasongwq
db9e35a7e1 add quick search in mobile layout (#2360)
Co-authored-by: jasongwq <jasongwq@126.com>
2021-11-20 20:55:52 +01:00
zadam
fe605c012a fix setting monospace font from theme 2021-11-20 13:20:06 +01:00
zadam
7a383a1314 create tray only for main window, not setup window 2021-11-20 12:52:23 +01:00
zadam
5290aab781 Merge remote-tracking branch 'origin/master' 2021-11-20 12:51:13 +01:00
zadam
86c3bbe5a2 Merge remote-tracking branch 'origin/stable' 2021-11-20 12:50:58 +01:00
zadam
4c7c53d8c8 retry for OpenNoteButtonWidget 2021-11-20 12:49:12 +01:00
Myzel394
21854b4a04 fixed title (#2356) 2021-11-19 13:03:12 +01:00
zadam
83f125a79f cleanup setting of utcDateCreated in branches and options code 2021-11-18 22:39:12 +01:00
zadam
e36bc42519 added changeId into entity_changes to have cross-sync change ID 2021-11-18 22:33:08 +01:00
zadam
15ac81627c DB cleanup migration 2021-11-18 21:52:56 +01:00
zadam
57fae2c8c6 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	package-lock.json
#	package.json
2021-11-18 21:36:03 +01:00
zadam
87b76abef9 better-sqlite3 binaries 2021-11-18 21:35:23 +01:00
zadam
d345b7ed56 added entity changes check after sync check failure, fixed sync 2021-11-17 22:57:09 +01:00
zadam
298af217e9 fix bug overwriting entity changes 2021-11-17 21:47:41 +01:00
zadam
89322c4b03 upgrade to electron v16 and node v16 2021-11-16 22:43:08 +01:00
zadam
7d64f6a7dd increment lastProcessedEntityChangeId correctly 2021-11-16 22:12:53 +01:00
zadam
b7efc92099 frontend library updates 2021-11-15 22:28:56 +01:00
zadam
bc8b6284a6 fix exporting root note, closes #2346
(cherry picked from commit 20a187fab9)
2021-11-15 21:28:12 +01:00
zadam
20a187fab9 fix exporting root note, closes #2346 2021-11-15 21:23:19 +01:00
zadam
0b001f41c0 improvements/simplification to the update check 2021-11-14 21:52:18 +01:00
zadam
242977c7a5 cleanup after update check merge 2021-11-14 13:45:37 +01:00
Myzel394
364ac331da Add update available box (#2329)
* current stand

* added update available button

* improved update available icon

* improved update available box

* adding server side version

* added backend

* fixed text

* added option handling

* added field disabling

* removed options

* fixed terminology

* removed unnecessary imports
2021-11-14 13:42:50 +01:00
Myzel394
fcc0a80f4e Add tray (#2325)
* copied tray from old thread

* added visibility changer to tray
2021-11-14 13:36:39 +01:00
zadam
8996f35cc0 Merge remote-tracking branch 'origin/next49' 2021-11-14 13:18:13 +01:00
zadam
980309ae2a sharing WIP 2021-10-19 22:48:38 +02:00
zadam
6a6bd4541a sharing WIP 2021-10-17 14:44:59 +02:00
zadam
a14aa461ca sharing WIP 2021-10-16 22:13:34 +02:00
159 changed files with 18283 additions and 4885 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
FROM node:14.18.1-alpine
FROM node:16.13.1-alpine
# Create app directory
WORKDIR /usr/src/app

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,2 @@
ALTER TABLE branches DROP COLUMN utcDateCreated;
ALTER TABLE options DROP COLUMN utcDateCreated;

View File

@@ -0,0 +1,23 @@
CREATE TABLE IF NOT EXISTS "mig_entity_changes" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entityName` TEXT NOT NULL,
`entityId` TEXT NOT NULL,
`hash` TEXT NOT NULL,
`isErased` INT NOT NULL,
`changeId` TEXT NOT NULL,
`sourceId` TEXT NOT NULL,
`isSynced` INTEGER NOT NULL,
`utcDateChanged` TEXT NOT NULL
);
INSERT INTO mig_entity_changes (entityName, entityId, hash, isErased, changeId, sourceId, isSynced, utcDateChanged)
SELECT entityName, entityId, hash, isErased, '', sourceId, isSynced, utcDateChanged FROM entity_changes;
DROP TABLE entity_changes;
ALTER TABLE mig_entity_changes RENAME TO entity_changes;
CREATE UNIQUE INDEX `IDX_entityChanges_entityName_entityId` ON "entity_changes" (
`entityName`,
`entityId`
);

View File

@@ -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
);

View File

@@ -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"

View File

@@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 952 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
libraries/normalize.min.css vendored Normal file
View File

@@ -0,0 +1,2 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}
/*# sourceMappingURL=normalize.min.css.map */

View File

@@ -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;
}();

File diff suppressed because one or more lines are too long

12633
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.48.6",
"version": "0.49.1-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -26,47 +26,49 @@
"dependencies": {
"archiver": "5.3.0",
"async-mutex": "0.3.2",
"axios": "0.23.0",
"better-sqlite3": "7.4.3",
"body-parser": "1.19.0",
"axios": "0.24.0",
"better-sqlite3": "7.4.5",
"body-parser": "1.19.1",
"chokidar": "3.5.2",
"cls-hooked": "4.2.2",
"commonmark": "0.30.0",
"cookie-parser": "1.4.5",
"cookie-parser": "1.4.6",
"csurf": "1.11.0",
"dayjs": "1.10.7",
"ejs": "3.1.6",
"electron-debug": "3.2.0",
"electron-dl": "3.2.1",
"electron-dl": "3.3.0",
"electron-find": "1.0.7",
"electron-window-state": "5.0.3",
"express": "4.17.1",
"@electron/remote": "2.0.1",
"express": "4.17.2",
"express-partial-content": "^1.0.2",
"express-rate-limit": "5.5.0",
"express-rate-limit": "5.5.1",
"express-session": "1.17.2",
"fs-extra": "10.0.0",
"helmet": "4.6.0",
"html": "1.0.0",
"html2plaintext": "2.1.2",
"html2plaintext": "2.1.4",
"http-proxy-agent": "5.0.0",
"https-proxy-agent": "5.0.0",
"image-type": "4.1.0",
"ini": "2.0.0",
"is-animated": "^2.0.1",
"is-svg": "4.3.1",
"is-svg": "4.3.2",
"jimp": "0.16.1",
"joplin-turndown-plugin-gfm": "1.0.12",
"jsdom": "18.0.0",
"mime-types": "2.1.33",
"multer": "1.4.3",
"node-abi": "3.2.0",
"open": "8.3.0",
"jsdom": "19.0.0",
"mime-types": "2.1.34",
"multer": "1.4.4",
"node-abi": "3.5.0",
"normalize-strings": "^1.1.1",
"open": "8.4.0",
"portscanner": "2.2.0",
"rand-token": "1.0.1",
"request": "^2.88.2",
"rimraf": "3.0.2",
"sanitize-filename": "1.6.3",
"sanitize-html": "2.5.2",
"sanitize-html": "2.6.1",
"sax": "1.2.4",
"semver": "7.3.5",
"serve-favicon": "2.5.0",
@@ -76,22 +78,22 @@
"tmp": "^0.2.1",
"turndown": "7.1.1",
"unescape": "1.0.1",
"ws": "8.2.3",
"ws": "8.4.0",
"yauzl": "2.10.0"
},
"devDependencies": {
"cross-env": "7.0.3",
"electron": "13.6.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"

View File

@@ -42,7 +42,7 @@ class NoteBuilder {
}
child(childNoteBuilder, prefix = "") {
new Branch(becca, {
new Branch({
branchId: id(),
noteId: childNoteBuilder.note.noteId,
parentNoteId: this.note.noteId,

View File

@@ -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");

View File

@@ -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();
});

View File

@@ -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

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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
};
}

View File

@@ -136,7 +136,10 @@ class Note extends AbstractEntity {
return this.parentBranches;
}
/** @returns {Branch[]} */
/**
* @returns {Branch[]}
* @deprecated use getParentBranches() instead
*/
getBranches() {
return this.parentBranches;
}

View File

@@ -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
}
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;
});

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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()

View File

@@ -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")

View File

@@ -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;
}
}

View File

@@ -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.");
}

View File

@@ -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");

View File

@@ -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))

View File

@@ -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;

View File

@@ -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?")) {

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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
};

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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.",
}
};

View File

@@ -0,0 +1,159 @@
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import linkService from "../services/link.js";
import server from "../services/server.js";
import froca from "../services/froca.js";
const TPL = `
<div class="backlinks-widget">
<style>
.backlinks-widget {
position: relative;
}
.backlinks-ticker {
position: absolute;
top: 10px;
right: 10px;
width: 130px;
border-radius: 10px;
border-color: var(--main-border-color);
background-color: var(--more-accented-background-color);
padding: 4px 10px 4px 10px;
opacity: 70%;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 10;
}
.backlinks-count {
cursor: pointer;
}
.backlinks-close-ticker {
cursor: pointer;
}
.backlinks-items {
z-index: 10;
position: absolute;
top: 50px;
right: 10px;
width: 400px;
border-radius: 10px;
background-color: #eeeeee;
color: #444;
padding: 20px;
overflow-y: auto;
}
.backlink-excerpt {
border-left: 2px solid var(--main-border-color);
padding-left: 10px;
opacity: 80%;
font-size: 90%;
}
.backlink-excerpt .backlink-link { /* the actual backlink */
font-weight: bold;
background-color: yellow;
}
/* relation map has already buttons in that position */
.type-relation-map .backlinks-ticker { top: 50px; }
.type-relation-map .backlinks-items { top: 100px; }
</style>
<div class="backlinks-ticker">
<span class="backlinks-count"></span>
<span class="bx bx-x backlinks-close-ticker"></span>
</div>
<div class="backlinks-items" style="display: none;"></div>
</div>
`;
export default class BacklinksWidget extends NoteContextAwareWidget {
doRender() {
this.$widget = $(TPL);
this.$count = this.$widget.find('.backlinks-count');
this.$items = this.$widget.find('.backlinks-items');
this.$ticker = this.$widget.find('.backlinks-ticker');
this.$count.on("click", () => {
this.$items.toggle();
this.$items.css("max-height", $(window).height() - this.$items.offset().top - 10);
if (this.$items.is(":visible")) {
this.renderBacklinks();
}
});
this.$closeTickerButton = this.$widget.find('.backlinks-close-ticker');
this.$closeTickerButton.on("click", () => {
this.$ticker.hide();
this.clearItems();
});
this.contentSized();
}
async refreshWithNote(note) {
this.clearItems();
// can't use froca since that would count only relations from loaded notes
const resp = await server.get(`notes/${this.noteId}/backlink-count`);
if (!resp || !resp.count) {
this.$ticker.hide();
return;
}
this.$ticker.show();
this.$count.text(
`${resp.count} backlink`
+ (resp.count === 1 ? '' : 's')
);
}
clearItems() {
this.$items.empty().hide();
}
async renderBacklinks() {
if (!this.note) {
return;
}
this.$items.empty();
const backlinks = await server.get(`note-map/${this.noteId}/backlinks`);
if (!backlinks.length) {
return;
}
await froca.getNotes(backlinks.map(bl => bl.noteId)); // prefetch all
for (const backlink of backlinks) {
const $item = $("<div>");
$item.append(await linkService.createNoteLink(backlink.noteId, {
showNoteIcon: true,
showNotePath: true,
showTooltip: false
}));
if (backlink.relationName) {
$item.append($("<p>").text("relation: " + backlink.relationName));
}
else {
$item.append(...backlink.excerpts);
}
this.$items.append($item);
}
}
}

View File

@@ -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
&nbsp;
<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
&nbsp;
<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}) {

View File

@@ -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) {

View File

@@ -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");
}
}

View File

@@ -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();
});

View File

@@ -0,0 +1,38 @@
import BasicWidget from "../basic_widget.js";
const TPL = `
<div style="display: none;">
<style>
.global-menu-button-update-available-button {
width: 21px !important;
height: 21px !important;
padding: 0 !important;
border-radius: 8px;
transform: scale(0.9);
border: none;
opacity: 0.8;
display: flex;
align-items: center;
justify-content: center;
}
.global-menu-button-wrapper:hover .global-menu-button-update-available-button {
opacity: 1;
}
</style>
<span class="bx bx-sync global-menu-button-update-available-button" title="Update available"></span>
</div>
`;
export default class UpdateAvailableWidget extends BasicWidget {
doRender() {
this.$widget = $(TPL);
}
updateVersionStatus(latestVersion) {
this.$widget.toggle(latestVersion > glob.triliumVersion);
}
}

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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() {

View File

@@ -0,0 +1,39 @@
import FlexContainer from "./containers/flex_container.js";
export default class NoteWrapperWidget extends FlexContainer {
constructor() {
super('column');
this.css("flex-grow", "1")
.collapsible();
}
doRender() {
super.doRender();
this.$widget.addClass("note-split");
}
setNoteContextEvent({noteContext}) {
this.refresh(noteContext);
}
noteSwitchedAndActivatedEvent({noteContext}) {
this.refresh(noteContext);
}
noteSwitchedEvent({noteContext}) {
this.refresh(noteContext);
}
activeContextChangedEvent({noteContext}) {
this.refresh(noteContext);
}
refresh(noteContext) {
this.$widget.toggleClass("full-content-width",
['image', 'mermaid', 'book', 'render'].includes(noteContext?.note?.type)
|| !!noteContext?.note?.hasLabel('fullContentWidth')
);
}
}

View File

@@ -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
&nbsp;
<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
&nbsp;
<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}) {

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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>`);

View File

@@ -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(" &nbsp;")
.append(addButton)
.append($addButton)
.append(" &nbsp;")
.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}) {

View File

@@ -0,0 +1,57 @@
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import options from "../services/options.js";
import attributeService from "../services/attributes.js";
const TPL = `
<div class="shared-info-widget alert alert-warning">
<style>
.shared-info-widget {
margin: 10px;
contain: none;
padding: 10px;
font-weight: bold;
}
</style>
<span class="share-text"></span> <a class="share-link external"></a>. For help visit <a href="https://github.com/zadam/trilium/wiki/Sharing">wiki</a>.
</div>`;
export default class SharedInfoWidget extends NoteContextAwareWidget {
isEnabled() {
return super.isEnabled() && this.noteId !== 'share' && this.note.hasAncestor('share');
}
doRender() {
this.$widget = $(TPL);
this.$shareLink = this.$widget.find(".share-link");
this.$shareText = this.$widget.find(".share-text");
this.contentSized();
}
async refreshWithNote(note) {
const syncServerHost = options.get("syncServerHost");
let link;
const shareId = note.getOwnedLabelValue('shareAlias') || note.noteId;
if (syncServerHost) {
link = syncServerHost + "/share/" + shareId;
this.$shareText.text("This note is shared publicly on");
}
else {
link = location.protocol + '//' + location.host + location.pathname + "share/" + shareId;
this.$shareText.text("This note is shared locally on");
}
this.$shareLink.attr("href", link).text(link);
}
entitiesReloadedEvent({loadResults}) {
if (loadResults.getAttributes().find(attr => attr.name.startsWith("share") && attributeService.isAffecting(attr, this.note))) {
this.refresh();
}
else if (loadResults.getBranches().find(branch => branch.noteId === this.noteId)) {
this.refresh();
}
}
}

View File

@@ -0,0 +1,71 @@
import SwitchWidget from "./switch.js";
import branchService from "../services/branches.js";
import server from "../services/server.js";
import utils from "../services/utils.js";
export default class SharedSwitchWidget extends SwitchWidget {
isEnabled() {
return super.isEnabled() && this.noteId !== 'root' && this.noteId !== 'share';
}
doRender() {
super.doRender();
this.$switchOnName.text("Shared");
this.$switchOnButton.attr("title", "Share the note");
this.$switchOffName.text("Shared");
this.$switchOffButton.attr("title", "Unshare the note");
this.$helpButton.attr("data-help-page", "Sharing").show();
this.$helpButton.on('click', e => utils.openHelp(e));
}
switchOn() {
branchService.cloneNoteTo(this.noteId, 'share');
}
async switchOff() {
const shareBranch = this.note.getParentBranches().find(b => b.parentNoteId === 'share');
if (!shareBranch) {
return;
}
if (this.note.getParentBranches().length === 1) {
const confirmDialog = await import('../dialogs/confirm.js');
const text = "This note exists only as a shared note, unsharing would delete it. Do you want to continue and thus delete this note?";
if (!await confirmDialog.confirm(text)) {
return;
}
}
await server.remove(`branches/${shareBranch.branchId}?taskId=no-progress-reporting`);
}
async refreshWithNote(note) {
const isShared = note.hasAncestor('share');
const canBeUnshared = isShared && note.getParentBranches().find(b => b.parentNoteId === 'share');
const switchDisabled = isShared && !canBeUnshared;
this.$switchOn.toggle(!isShared);
this.$switchOff.toggle(!!isShared);
if (switchDisabled) {
this.$widget.attr("title", "Note cannot be unshared here because it is shared through inheritance from an ancestor.");
this.$switchOff.addClass("switch-disabled");
}
else {
this.$widget.removeAttr("title");
this.$switchOff.removeClass("switch-disabled");
}
}
entitiesReloadedEvent({loadResults}) {
if (loadResults.getBranches().find(b => b.noteId === this.noteId)) {
this.refresh();
}
}
}

View File

@@ -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>
&nbsp;
<span class="switch-on-button">
<label class="switch">
<span class="slider"></span>
</span>
</div>
<div class="switch-off">
<span class="switch-off-name"></span>
&nbsp;
<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() {}
}

View File

@@ -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);

View File

@@ -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();
});
}

View File

@@ -14,20 +14,10 @@ const TPL = `
position: relative;
}
.trilium-api-docs-button {
/*display: none;*/
position: absolute;
top: 10px;
right: 10px;
}
.note-detail-code-editor {
min-height: 50px;
}
</style>
<button class="btn bx bx-help-circle trilium-api-docs-button icon-button floating-button"
title="Open Trilium API docs"></button>
<div class="note-detail-code-editor"></div>
@@ -37,6 +27,13 @@ const TPL = `
Execute <kbd data-command="runActiveNote"></kbd>
</button>
<button class="no-print trilium-api-docs-button btn btn-sm"
title="Open Trilium API docs">
<span class="bx bx-help-circle"></span>
API docs
</button>
<button class="no-print save-to-note-button btn btn-sm">
<span class="bx bx-save"></span>

View File

@@ -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');
}
}
}

View File

@@ -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}) {

View File

@@ -0,0 +1,110 @@
body {
font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif;
}
#layout {
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: row-reverse;
}
#menu {
padding: 25px;
flex-basis: 0;
flex-grow: 1;
overflow: auto;
}
#menu p {
margin: 0;
}
#menu > p {
font-weight: bold;
font-size: 110%;
}
#menu ul {
padding-left: 20px;
}
#main {
flex-basis: 0;
flex-grow: 3;
}
#title {
margin: 0;
padding: 20px 20px 0 20px;
}
#content {
padding: 20px;
}
img {
max-width: 100%;
}
pre {
white-space: pre-wrap;
word-wrap: anywhere;
}
iframe.pdf-view {
width: 100%;
height: 800px;
}
#menuButton {
display: none;
position: fixed;
top: 8px;
left: 5px;
width: 1.4em;
border-radius: 5px;
border: 1px solid #aaa;
font-size: 2rem;
z-index: 10;
height: auto;
color: black;
cursor: pointer;
}
#menuButton::after {
position: relative;
top: -2px;
left: 1px;
}
@media (max-width: 48em) {
#layout.navMenu #menu {
display: block;
margin-top: 40px;
}
#menuButton {
display: block;
}
#layout.navMenu #main {
display: none;
}
#title {
padding-left: 60px;
}
#layout.navMenu #menuButton::after {
content: "«";
}
#menuButton::after {
content: "»";
}
#menu {
display: none;
}
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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());

View File

@@ -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());
}

Some files were not shown because too many files have changed in this diff Show More