mirror of
https://github.com/zadam/trilium.git
synced 2025-10-30 18:05:55 +01:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7672f22ce0 | ||
|
|
ef37a52a06 | ||
|
|
2318d615bb | ||
|
|
bc14c3d665 | ||
|
|
b89ea9a684 | ||
|
|
496767a52b | ||
|
|
9139c597e5 | ||
|
|
942132c01d | ||
|
|
1862acd1ff | ||
|
|
ce7e18d0b0 | ||
|
|
65280d5ba3 | ||
|
|
7e3d424e23 | ||
|
|
bff04c121a | ||
|
|
e8903e82a1 | ||
|
|
0cfd95d9b8 | ||
|
|
1aa5349628 | ||
|
|
4e21d12202 |
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"version": "0.46.4-beta",
|
||||
"version": "0.46.5",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.46.5",
|
||||
"version": "0.46.7",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start-server": "cross-env TRILIUM_ENV=dev node ./src/www",
|
||||
"start-electron": "cross-env TRILIUM_ENV=dev electron .",
|
||||
"start-electron": "cross-env TRILIUM_ENV=dev electron --inspect=5858 .",
|
||||
"build-backend-docs": "./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js",
|
||||
"build-frontend-docs": "./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/app/entities/*.js src/public/app/services/frontend_script_api.js src/public/app/widgets/collapsible_widget.js",
|
||||
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs",
|
||||
|
||||
@@ -49,7 +49,12 @@ class Note extends Entity {
|
||||
this.isContentAvailable = protectedSessionService.isProtectedSessionAvailable();
|
||||
|
||||
if (this.isContentAvailable) {
|
||||
this.title = protectedSessionService.decryptString(this.title);
|
||||
try {
|
||||
this.title = protectedSessionService.decryptString(this.title);
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Could not decrypt title of note ${this.noteId}: ${e.message} ${e.stack}`)
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.title = "[protected]";
|
||||
@@ -156,14 +161,14 @@ class Note extends Entity {
|
||||
|
||||
sql.upsert("note_contents", "noteId", pojo);
|
||||
|
||||
const hash = utils.hash(this.noteId + "|" + content.toString());
|
||||
const hash = utils.hash(this.noteId + "|" + pojo.content.toString());
|
||||
|
||||
entityChangesService.addEntityChange({
|
||||
entityName: 'note_contents',
|
||||
entityId: this.noteId,
|
||||
hash: hash,
|
||||
isErased: false,
|
||||
utcDateChanged: this.getUtcDateChanged()
|
||||
utcDateChanged: pojo.utcDateModified
|
||||
}, null);
|
||||
}
|
||||
|
||||
|
||||
@@ -359,7 +359,7 @@ class NoteShort {
|
||||
const workspaceIconClass = this.getWorkspaceIconClass();
|
||||
|
||||
if (iconClassLabels.length > 0) {
|
||||
return iconClassLabels.map(l => l.value).join(' ');
|
||||
return iconClassLabels[0].value;
|
||||
}
|
||||
else if (workspaceIconClass) {
|
||||
return workspaceIconClass;
|
||||
|
||||
@@ -83,6 +83,9 @@ export default class MobileLayout {
|
||||
.child(new NoteTitleWidget())
|
||||
.child(new CloseDetailButtonWidget()))
|
||||
.child(new NoteDetailWidget()
|
||||
.css('padding', '5px 20px 10px 0')));
|
||||
.css('padding', '5px 20px 10px 0')
|
||||
.css('overflow', 'auto')
|
||||
.css('height', '100%')
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import utils from "./utils.js";
|
||||
import options from './options.js';
|
||||
import server from "./server.js";
|
||||
|
||||
const PROTECTED_SESSION_ID_KEY = 'protectedSessionId';
|
||||
|
||||
@@ -23,11 +24,11 @@ function resetSessionCookie() {
|
||||
utils.setSessionCookie(PROTECTED_SESSION_ID_KEY, null);
|
||||
}
|
||||
|
||||
function resetProtectedSession() {
|
||||
async function resetProtectedSession() {
|
||||
resetSessionCookie();
|
||||
|
||||
// most secure solution - guarantees nothing remained in memory
|
||||
// since this expires because user doesn't use the app, it shouldn't be disruptive
|
||||
await server.post("logout/protected");
|
||||
|
||||
utils.reloadApp();
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export default class NoteTitleWidget extends TabAwareWidget {
|
||||
|
||||
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
|
||||
|
||||
await server.put(`notes/${this.noteId}/change-title`, {title});
|
||||
await server.put(`notes/${this.noteId}/change-title`, {title}, this.componentId);
|
||||
});
|
||||
|
||||
appContext.addBeforeUnloadListener(this);
|
||||
|
||||
@@ -176,6 +176,15 @@ const TPL = `
|
||||
title="Images which are shown in the parent text note will not be displayed in the tree"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input auto-collapse-note-tree" type="checkbox" value="">
|
||||
|
||||
Automatically collapse notes
|
||||
<span class="bx bx-info-circle"
|
||||
title="Notes will be collapsed after period of inactivity to declutter the tree."></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -235,6 +244,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
this.$treeSettingsPopup = this.$widget.find('.tree-settings-popup');
|
||||
this.$hideArchivedNotesCheckbox = this.$treeSettingsPopup.find('.hide-archived-notes');
|
||||
this.$hideIncludedImages = this.$treeSettingsPopup.find('.hide-included-images');
|
||||
this.$autoCollapseNoteTree = this.$treeSettingsPopup.find('.auto-collapse-note-tree');
|
||||
|
||||
this.$treeSettingsButton = this.$widget.find('.tree-settings-button');
|
||||
this.$treeSettingsButton.on("click", e => {
|
||||
@@ -245,6 +255,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
|
||||
this.$hideArchivedNotesCheckbox.prop("checked", this.hideArchivedNotes);
|
||||
this.$hideIncludedImages.prop("checked", this.hideIncludedImages);
|
||||
this.$autoCollapseNoteTree.prop("checked", this.autoCollapseNoteTree);
|
||||
|
||||
let top = this.$treeSettingsButton[0].offsetTop;
|
||||
let left = this.$treeSettingsButton[0].offsetLeft;
|
||||
@@ -272,6 +283,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
this.$saveTreeSettingsButton.on('click', async () => {
|
||||
await this.setHideArchivedNotes(this.$hideArchivedNotesCheckbox.prop("checked"));
|
||||
await this.setHideIncludedImages(this.$hideIncludedImages.prop("checked"));
|
||||
await this.setAutoCollapseNoteTree(this.$autoCollapseNoteTree.prop("checked"));
|
||||
|
||||
this.$treeSettingsPopup.hide();
|
||||
|
||||
@@ -327,6 +339,14 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
await options.save("hideIncludedImages_" + this.treeName, val.toString());
|
||||
}
|
||||
|
||||
get autoCollapseNoteTree() {
|
||||
return options.is("autoCollapseNoteTree");
|
||||
}
|
||||
|
||||
async setAutoCollapseNoteTree(val) {
|
||||
await options.save("autoCollapseNoteTree", val.toString());
|
||||
}
|
||||
|
||||
initFancyTree() {
|
||||
const treeData = [this.prepareRootNode()];
|
||||
|
||||
@@ -797,8 +817,10 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
|
||||
const node = await this.expandToNote(activeContext.notePath);
|
||||
|
||||
await node.makeVisible({scrollIntoView: true});
|
||||
node.setActive(true, {noEvents: true, noFocus: false});
|
||||
if (node) {
|
||||
await node.makeVisible({scrollIntoView: true});
|
||||
node.setActive(true, {noEvents: true, noFocus: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,7 +873,11 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
// these are real notes with real notePath, user can display them in a detail
|
||||
// but they don't have a node in the tree
|
||||
|
||||
ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`);
|
||||
const childNote = await treeCache.getNote(childNoteId);
|
||||
|
||||
if (!childNote || childNote.type !== 'image') {
|
||||
ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -955,6 +981,10 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
}
|
||||
|
||||
this.autoCollapseTimeoutId = setTimeout(() => {
|
||||
if (!this.autoCollapseNoteTree) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* We're collapsing notes after period of inactivity to "cleanup" the tree - users rarely
|
||||
* collapse the notes and the tree becomes unusuably large.
|
||||
|
||||
@@ -14,6 +14,10 @@ const TPL = `
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.standard-top-widget > div {
|
||||
flex-shrink: 0; /* fixes https://github.com/zadam/trilium/issues/1745 */
|
||||
}
|
||||
|
||||
.standard-top-widget button.noborder {
|
||||
padding: 1px 5px 1px 5px;
|
||||
font-size: 90%;
|
||||
|
||||
@@ -5,6 +5,7 @@ const TPL = `
|
||||
<style>
|
||||
.note-detail-read-only-code {
|
||||
position: relative;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.note-detail-read-only-code-content {
|
||||
|
||||
@@ -25,6 +25,7 @@ const TPL = `
|
||||
padding-top: 10px;
|
||||
font-family: var(--detail-text-font-family);
|
||||
position: relative;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.note-detail-readonly-text p:first-child, .note-detail-readonly-text::before {
|
||||
|
||||
@@ -78,6 +78,12 @@ function loginToProtectedSession(req) {
|
||||
};
|
||||
}
|
||||
|
||||
function logoutFromProtectedSession() {
|
||||
protectedSessionService.resetDataKey();
|
||||
|
||||
eventService.emit(eventService.LEAVE_PROTECTED_SESSION);
|
||||
}
|
||||
|
||||
function token(req) {
|
||||
const username = req.body.username;
|
||||
const password = req.body.password;
|
||||
@@ -101,5 +107,6 @@ function token(req) {
|
||||
module.exports = {
|
||||
loginSync,
|
||||
loginToProtectedSession,
|
||||
logoutFromProtectedSession,
|
||||
token
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ const noteCacheService = require('../../services/note_cache/note_cache_service')
|
||||
const protectedSessionService = require('../../services/protected_session');
|
||||
const noteRevisionService = require('../../services/note_revisions');
|
||||
const utils = require('../../services/utils');
|
||||
const sql = require('../../services/sql');
|
||||
const path = require('path');
|
||||
|
||||
function getNoteRevisions(req) {
|
||||
|
||||
@@ -41,7 +41,8 @@ const ALLOWED_OPTIONS = new Set([
|
||||
'attributeListExpanded',
|
||||
'promotedAttributesExpanded',
|
||||
'similarNotesExpanded',
|
||||
'headingStyle'
|
||||
'headingStyle',
|
||||
'autoCollapseNoteTree'
|
||||
]);
|
||||
|
||||
function getOptions() {
|
||||
|
||||
@@ -270,6 +270,8 @@ function register(app) {
|
||||
route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler);
|
||||
// this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
|
||||
apiRoute(POST, '/api/login/protected', loginApiRoute.loginToProtectedSession);
|
||||
apiRoute(POST, '/api/logout/protected', loginApiRoute.logoutFromProtectedSession);
|
||||
|
||||
route(POST, '/api/login/token', [], loginApiRoute.token, apiResultHandler);
|
||||
|
||||
// in case of local electron, local calls are allowed unauthenticated, for server they need auth
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2021-03-14T22:56:27+01:00", buildRevision: "6c8d20288df302f3a415bd1bdcace98bf29d4bf6" };
|
||||
module.exports = { buildDate:"2021-04-03T22:37:04+02:00", buildRevision: "ef37a52a06b471e60f9c0f11da704283bbcef6ab" };
|
||||
|
||||
@@ -2,6 +2,7 @@ const log = require('./log');
|
||||
|
||||
const NOTE_TITLE_CHANGED = "NOTE_TITLE_CHANGED";
|
||||
const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION";
|
||||
const LEAVE_PROTECTED_SESSION = "LEAVE_PROTECTED_SESSION";
|
||||
const ENTITY_CREATED = "ENTITY_CREATED";
|
||||
const ENTITY_CHANGED = "ENTITY_CHANGED";
|
||||
const ENTITY_DELETED = "ENTITY_DELETED";
|
||||
@@ -47,6 +48,7 @@ module.exports = {
|
||||
// event types:
|
||||
NOTE_TITLE_CHANGED,
|
||||
ENTER_PROTECTED_SESSION,
|
||||
LEAVE_PROTECTED_SESSION,
|
||||
ENTITY_CREATED,
|
||||
ENTITY_CHANGED,
|
||||
ENTITY_DELETED,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const protectedSessionService = require('../../protected_session');
|
||||
const log = require('../../log');
|
||||
|
||||
class Note {
|
||||
constructor(noteCache, row) {
|
||||
@@ -405,7 +406,7 @@ class Note {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let minDistance = 999_999;
|
||||
let minDistance = 999999;
|
||||
|
||||
for (const parent of this.parents) {
|
||||
minDistance = Math.min(minDistance, parent.getDistanceToAncestor(ancestorNoteId) + 1);
|
||||
@@ -416,9 +417,14 @@ class Note {
|
||||
|
||||
decrypt() {
|
||||
if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
|
||||
this.title = protectedSessionService.decryptString(this.title);
|
||||
try {
|
||||
this.title = protectedSessionService.decryptString(this.title);
|
||||
|
||||
this.isDecrypted = true;
|
||||
this.isDecrypted = true;
|
||||
}
|
||||
catch (e) {
|
||||
log.error(`Could not decrypt note ${this.noteId}: ${e.message} ${e.stack}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -177,6 +177,10 @@ eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
|
||||
}
|
||||
});
|
||||
|
||||
eventService.subscribe(eventService.LEAVE_PROTECTED_SESSION, () => {
|
||||
load();
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
load
|
||||
};
|
||||
|
||||
@@ -233,7 +233,7 @@ async function findSimilarNotes(noteId) {
|
||||
|
||||
const baseNote = noteCache.notes[noteId];
|
||||
|
||||
if (!baseNote) {
|
||||
if (!baseNote || !baseNote.utcDateCreated) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ const defaultOptions = [
|
||||
{ name: 'similarNotesExpanded', value: 'true', isSynced: true },
|
||||
{ name: 'debugModeEnabled', value: 'false', isSynced: false },
|
||||
{ name: 'headingStyle', value: 'markdown', isSynced: true },
|
||||
{ name: 'autoCollapseNoteTree', value: 'true', isSynced: true },
|
||||
];
|
||||
|
||||
function initStartupOptions() {
|
||||
|
||||
@@ -5,7 +5,7 @@ const log = require('./log');
|
||||
const dataEncryptionService = require('./data_encryption');
|
||||
const cls = require('./cls');
|
||||
|
||||
const dataKeyMap = {};
|
||||
let dataKeyMap = {};
|
||||
|
||||
function setDataKey(decryptedDataKey) {
|
||||
const protectedSessionId = utils.randomSecureToken(32);
|
||||
@@ -29,6 +29,10 @@ function getDataKey() {
|
||||
return dataKeyMap[protectedSessionId];
|
||||
}
|
||||
|
||||
function resetDataKey() {
|
||||
dataKeyMap = {};
|
||||
}
|
||||
|
||||
function isProtectedSessionAvailable() {
|
||||
const protectedSessionId = getProtectedSessionId();
|
||||
|
||||
@@ -71,6 +75,7 @@ function decryptString(cipherText) {
|
||||
module.exports = {
|
||||
setDataKey,
|
||||
getDataKey,
|
||||
resetDataKey,
|
||||
isProtectedSessionAvailable,
|
||||
encrypt,
|
||||
decrypt,
|
||||
|
||||
@@ -26,9 +26,7 @@ function updateEntity(entityChange, entity, sourceId) {
|
||||
? updateNoteReordering(entityChange, entity, sourceId)
|
||||
: updateNormalEntity(entityChange, entity, sourceId);
|
||||
|
||||
// currently making exception for protected notes and note revisions because here
|
||||
// the title and content are not available decrypted as listeners would expect
|
||||
if (updated && !entity.isProtected && !entityChange.isErased) {
|
||||
if (updated && !entityChange.isErased) {
|
||||
eventService.emit(eventService.ENTITY_SYNCED, {
|
||||
entityName: entityChange.entityName,
|
||||
entity
|
||||
@@ -44,7 +42,7 @@ function updateNormalEntity(remoteEntityChange, entity, sourceId) {
|
||||
|
||||
if (localEntityChange && !localEntityChange.isErased && remoteEntityChange.isErased) {
|
||||
sql.transactional(() => {
|
||||
const primaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName;
|
||||
const primaryKey = entityConstructor.getEntityFromEntityName(remoteEntityChange.entityName).primaryKeyName;
|
||||
|
||||
sql.execute(`DELETE FROM ${remoteEntityChange.entityName} WHERE ${primaryKey} = ?`, remoteEntityChange.entityId);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const sql = require('./sql');
|
||||
const log = require('./log');
|
||||
const repository = require('./repository');
|
||||
const Branch = require('../entities/branch');
|
||||
const entityChangesService = require('./entity_changes.js');
|
||||
@@ -139,7 +140,12 @@ function sortNotesByTitle(parentNoteId, foldersFirst = false, reverse = false) {
|
||||
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?",
|
||||
[position, note.branchId]);
|
||||
|
||||
noteCache.branches[note.branchId].notePosition = position;
|
||||
if (note.branchId in noteCache.branches) {
|
||||
noteCache.branches[note.branchId].notePosition = position;
|
||||
}
|
||||
else {
|
||||
log.info(`Branch "${note.branchId}" was not found in note cache.`);
|
||||
}
|
||||
|
||||
position += 10;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user