mirror of
https://github.com/TriliumNext/Notes.git
synced 2026-01-07 05:19:53 -06:00
Merge branch 'master' into excali-17-2
This commit is contained in:
@@ -161,6 +161,13 @@ class BRevision extends AbstractBeccaEntity {
|
||||
return this.getAttachments().filter(attachment => attachment.title === title)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Revisions are not soft-deletable, they are immediately hard-deleted (erased).
|
||||
*/
|
||||
eraseRevision() {
|
||||
require("../../services/erase.js").eraseRevisions([this.revisionId]);
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
|
||||
@@ -427,6 +427,116 @@ paths:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
/attachments:
|
||||
post:
|
||||
description: create an attachment
|
||||
operationId: postAttachment
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateAttachment'
|
||||
responses:
|
||||
'201':
|
||||
description: attachment created
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Attachment'
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
/attachments/{attachmentId}:
|
||||
parameters:
|
||||
- name: attachmentId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/components/schemas/EntityId'
|
||||
get:
|
||||
description: Returns an attachment identified by its ID
|
||||
operationId: getAttachmentById
|
||||
responses:
|
||||
'200':
|
||||
description: attachment response
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Attachment'
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
patch:
|
||||
description: patch an attachment identified by the attachmentId with changes in the body. Only role, mime, title, and position are patchable.
|
||||
operationId: patchAttachmentById
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Attachment'
|
||||
responses:
|
||||
'200':
|
||||
description: attribute updated
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Attachment'
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
delete:
|
||||
description: deletes an attachment based on the attachmentId supplied.
|
||||
operationId: deleteAttachmentById
|
||||
responses:
|
||||
'204':
|
||||
description: attachment deleted
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
/attachments/{attachmentId}/content:
|
||||
parameters:
|
||||
- name: attachmentId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/components/schemas/EntityId'
|
||||
get:
|
||||
description: Returns attachment content identified by its ID
|
||||
operationId: getAttachmentContent
|
||||
responses:
|
||||
'200':
|
||||
description: attachment content response
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
put:
|
||||
description: Updates attachment content identified by its ID
|
||||
operationId: putAttachmentContentById
|
||||
requestBody:
|
||||
description: html content of attachment
|
||||
required: true
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'204':
|
||||
description: attachment content updated
|
||||
/attributes:
|
||||
post:
|
||||
description: create an attribute for a given note
|
||||
@@ -474,7 +584,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
patch:
|
||||
description: patch a attribute identified by the attributeId with changes in the body. For labels, only value and position can be updated. For relations, only position can be updated. If you want to modify other properties, you need to delete the old attribute and create a new one.
|
||||
description: patch an attribute identified by the attributeId with changes in the body. For labels, only value and position can be updated. For relations, only position can be updated. If you want to modify other properties, you need to delete the old attribute and create a new one.
|
||||
operationId: patchAttributeById
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -496,7 +606,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
delete:
|
||||
description: deletes a attribute based on the attributeId supplied.
|
||||
description: deletes an attribute based on the attributeId supplied.
|
||||
operationId: deleteAttributeById
|
||||
responses:
|
||||
'204':
|
||||
@@ -884,6 +994,57 @@ components:
|
||||
$ref: '#/components/schemas/Note'
|
||||
branch:
|
||||
$ref: '#/components/schemas/Branch'
|
||||
Attachment:
|
||||
type: object
|
||||
description: Attachment is owned by a note, has title and content
|
||||
properties:
|
||||
attachmentId:
|
||||
$ref: '#/components/schemas/EntityId'
|
||||
readOnly: true
|
||||
ownerId:
|
||||
$ref: '#/components/schemas/EntityId'
|
||||
description: identifies the owner of the attachment, is either noteId or revisionId
|
||||
role:
|
||||
type: string
|
||||
mime:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
position:
|
||||
type: integer
|
||||
format: int32
|
||||
blobId:
|
||||
type: string
|
||||
description: ID of the blob object which effectively serves as a content hash
|
||||
dateModified:
|
||||
$ref: '#/components/schemas/LocalDateTime'
|
||||
readOnly: true
|
||||
utcDateModified:
|
||||
$ref: '#/components/schemas/UtcDateTime'
|
||||
readOnly: true
|
||||
utcDateScheduledForErasureSince:
|
||||
$ref: '#/components/schemas/UtcDateTime'
|
||||
readOnly: true
|
||||
contentLength:
|
||||
type: integer
|
||||
format: int32
|
||||
CreateAttachment:
|
||||
type: object
|
||||
properties:
|
||||
ownerId:
|
||||
$ref: '#/components/schemas/EntityId'
|
||||
description: identifies the owner of the attachment, is either noteId or revisionId
|
||||
role:
|
||||
type: string
|
||||
mime:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
position:
|
||||
type: integer
|
||||
format: int32
|
||||
Attribute:
|
||||
type: object
|
||||
description: Attribute (Label, Relation) is a key-value record attached to a note.
|
||||
|
||||
44
src/public/app/menus/image_context_menu.js
Normal file
44
src/public/app/menus/image_context_menu.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import utils from "../services/utils.js";
|
||||
import contextMenu from "./context_menu.js";
|
||||
import imageService from "../services/image.js";
|
||||
|
||||
const PROP_NAME = "imageContextMenuInstalled";
|
||||
|
||||
function setupContextMenu($image) {
|
||||
if (!utils.isElectron() || $image.prop(PROP_NAME)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$image.prop(PROP_NAME, true);
|
||||
$image.on('contextmenu', e => {
|
||||
e.preventDefault();
|
||||
|
||||
contextMenu.show({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
{
|
||||
title: "Copy reference to clipboard",
|
||||
command: "copyImageReferenceToClipboard",
|
||||
uiIcon: "bx bx-empty"
|
||||
},
|
||||
{title: "Copy image to clipboard", command: "copyImageToClipboard", uiIcon: "bx bx-empty"},
|
||||
],
|
||||
selectMenuItemHandler: ({command}) => {
|
||||
if (command === 'copyImageReferenceToClipboard') {
|
||||
imageService.copyImageReferenceToClipboard($image);
|
||||
} else if (command === 'copyImageToClipboard') {
|
||||
const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
|
||||
utils.dynamicRequire('electron');
|
||||
webContents.copyImageAt(e.pageX, e.pageY);
|
||||
} else {
|
||||
throw new Error(`Unrecognized command '${command}'`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
setupContextMenu
|
||||
};
|
||||
@@ -107,6 +107,13 @@ async function deleteNotes(branchIdsToDelete, forceDeleteAllClones = false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await activateParentNotePath();
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
const taskId = utils.randomString(10);
|
||||
|
||||
let counter = 0;
|
||||
@@ -134,6 +141,16 @@ async function deleteNotes(branchIdsToDelete, forceDeleteAllClones = false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function activateParentNotePath() {
|
||||
// this is not perfect, maybe we should find the next/previous sibling, but that's more complex
|
||||
const activeContext = appContext.tabManager.getActiveContext();
|
||||
const parentNotePathArr = activeContext.notePathArray.slice(0, -1);
|
||||
|
||||
if (parentNotePathArr.length > 0) {
|
||||
activeContext.setNote(parentNotePathArr.join("/"));
|
||||
}
|
||||
}
|
||||
|
||||
async function moveNodeUpInHierarchy(node) {
|
||||
if (hoistedNoteService.isHoistedNode(node)
|
||||
|| hoistedNoteService.isTopLevelNode(node)
|
||||
|
||||
@@ -9,6 +9,7 @@ import linkService from "./link.js";
|
||||
import treeService from "./tree.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
import FAttachment from "../entities/fattachment.js";
|
||||
import imageContextMenuService from "../menus/image_context_menu.js";
|
||||
|
||||
let idCounter = 1;
|
||||
|
||||
@@ -148,6 +149,8 @@ function renderImage(entity, $renderedContent, options = {}) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
imageContextMenuService.setupContextMenu($img);
|
||||
}
|
||||
|
||||
function renderFile(entity, type, $renderedContent) {
|
||||
|
||||
@@ -48,11 +48,11 @@ async function checkNoteAccess(notePath, noteContext) {
|
||||
|
||||
const hoistedNoteId = noteContext.hoistedNoteId;
|
||||
|
||||
if (!resolvedNotePath.includes(hoistedNoteId) && !resolvedNotePath.includes('_hidden')) {
|
||||
if (!resolvedNotePath.includes(hoistedNoteId) && (!resolvedNotePath.includes('_hidden') || resolvedNotePath.includes('_lbBookmarks'))) {
|
||||
const requestedNote = await froca.getNote(treeService.getNoteIdFromUrl(resolvedNotePath));
|
||||
const hoistedNote = await froca.getNote(hoistedNoteId);
|
||||
|
||||
if (!hoistedNote.hasAncestor('_hidden')
|
||||
if ((!hoistedNote.hasAncestor('_hidden') || resolvedNotePath.includes('_lbBookmarks'))
|
||||
&& !await dialogService.confirm(`Requested note '${requestedNote.title}' is outside of hoisted note '${hoistedNote.title}' subtree and you must unhoist to access the note. Do you want to proceed with unhoisting?`)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import froca from "./froca.js";
|
||||
import attributeRenderer from "./attribute_renderer.js";
|
||||
import libraryLoader from "./library_loader.js";
|
||||
import treeService from "./tree.js";
|
||||
import utils from "./utils.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-list">
|
||||
@@ -215,7 +216,11 @@ class NoteListRenderer {
|
||||
if (highlightedTokens.length > 0) {
|
||||
await libraryLoader.requireLibrary(libraryLoader.MARKJS);
|
||||
|
||||
this.highlightRegex = new RegExp(highlightedTokens.join("|"), 'gi');
|
||||
const regex = highlightedTokens
|
||||
.map(token => utils.escapeRegExp(token))
|
||||
.join("|");
|
||||
|
||||
this.highlightRegex = new RegExp(regex, 'gi');
|
||||
} else {
|
||||
this.highlightRegex = null;
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
|
||||
this.attributeDetailWidget.hide();
|
||||
});
|
||||
|
||||
this.$editor.on('blur', () => this.save());
|
||||
this.$editor.on('blur', () => setTimeout(() => this.save(), 100)); // Timeout to fix https://github.com/zadam/trilium/issues/4160
|
||||
|
||||
this.$addNewAttributeButton = this.$widget.find('.add-new-attribute-button');
|
||||
this.$addNewAttributeButton.on('click', e => this.addNewAttribute(e));
|
||||
|
||||
@@ -8,10 +8,13 @@ export default class RightPaneContainer extends FlexContainer {
|
||||
this.id('right-pane');
|
||||
this.css('height', '100%');
|
||||
this.collapsible();
|
||||
|
||||
this.rightPaneHidden = false;
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled()
|
||||
&& !this.rightPaneHidden
|
||||
&& this.children.length > 0
|
||||
&& !!this.children.find(ch => ch.isEnabled() && ch.canBeShown());
|
||||
}
|
||||
@@ -44,4 +47,10 @@ export default class RightPaneContainer extends FlexContainer {
|
||||
splitService.setupRightPaneResizer();
|
||||
}
|
||||
}
|
||||
|
||||
toggleRightPaneEvent() {
|
||||
this.rightPaneHidden = !this.rightPaneHidden;
|
||||
|
||||
this.reEvaluateRightPaneVisibilityCommand();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1091,7 +1091,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeCtx = this.#getActiveNodeCtx();
|
||||
const activeNode = this.getActiveNode();
|
||||
const activeNodeFocused = activeNode?.hasFocus();
|
||||
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
|
||||
|
||||
const refreshCtx = {
|
||||
noteIdsToUpdate: new Set(),
|
||||
@@ -1108,7 +1110,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
|
||||
await this.#executeTreeUpdates(refreshCtx, loadResults);
|
||||
|
||||
await this.#setActiveNode(nodeCtx, movedActiveNode, parentsOfAddedNodes);
|
||||
await this.#setActiveNode(activeNotePath, activeNodeFocused, movedActiveNode, parentsOfAddedNodes);
|
||||
|
||||
if (refreshCtx.noteIdsToReload.size > 0 || refreshCtx.noteIdsToUpdate.size > 0) {
|
||||
// workaround for https://github.com/mar10/fancytree/issues/1054
|
||||
@@ -1257,73 +1259,42 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
#getActiveNodeCtx() {
|
||||
const nodeCtx = {
|
||||
activeNotePath: null,
|
||||
activeNodeFocused: null,
|
||||
nextNotePath: null
|
||||
};
|
||||
|
||||
const activeNode = this.getActiveNode();
|
||||
nodeCtx.activeNodeFocused = activeNode?.hasFocus();
|
||||
nodeCtx.activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
|
||||
const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
|
||||
nodeCtx.nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null;
|
||||
return nodeCtx;
|
||||
}
|
||||
|
||||
async #setActiveNode(nodeCtx, movedActiveNode, parentsOfAddedNodes) {
|
||||
async #setActiveNode(activeNotePath, activeNodeFocused, movedActiveNode, parentsOfAddedNodes) {
|
||||
if (movedActiveNode) {
|
||||
for (const parentNode of parentsOfAddedNodes) {
|
||||
const foundNode = (parentNode.getChildren() || []).find(child => child.data.noteId === movedActiveNode.data.noteId);
|
||||
if (foundNode) {
|
||||
nodeCtx.activeNotePath = treeService.getNotePath(foundNode);
|
||||
activeNotePath = treeService.getNotePath(foundNode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!nodeCtx.activeNotePath) {
|
||||
if (!activeNotePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
let node = await this.expandToNote(nodeCtx.activeNotePath, false);
|
||||
if (node && node.data.noteId !== treeService.getNoteIdFromUrl(nodeCtx.activeNotePath)) {
|
||||
let node = await this.expandToNote(activeNotePath, false);
|
||||
|
||||
if (node && node.data.noteId !== treeService.getNoteIdFromUrl(activeNotePath)) {
|
||||
// if the active note has been moved elsewhere then it won't be found by the path,
|
||||
// so we switch to the alternative of trying to find it by noteId
|
||||
const notesById = this.getNodesByNoteId(treeService.getNoteIdFromUrl(nodeCtx.activeNotePath));
|
||||
const notesById = this.getNodesByNoteId(treeService.getNoteIdFromUrl(activeNotePath));
|
||||
|
||||
// if there are multiple clones, then we'd rather not activate anyone
|
||||
node = notesById.length === 1 ? notesById[0] : null;
|
||||
}
|
||||
|
||||
if (node) {
|
||||
if (nodeCtx.activeNodeFocused) {
|
||||
// needed by Firefox: https://github.com/zadam/trilium/issues/1865
|
||||
this.tree.$container.focus();
|
||||
}
|
||||
|
||||
await node.setActive(true, {noEvents: true, noFocus: !nodeCtx.activeNodeFocused});
|
||||
} else {
|
||||
// this is used when the original note has been deleted, and we want to move the focus to the note above/below
|
||||
node = await this.expandToNote(nodeCtx.nextNotePath, false);
|
||||
|
||||
if (node) {
|
||||
// FIXME: this is conceptually wrong
|
||||
// here note tree is responsible for updating global state of the application
|
||||
// this should be done by NoteContext / TabManager and note tree should only listen to
|
||||
// changes in active note and just set the "active" state
|
||||
// We don't await since that can bring up infinite cycles when e.g. custom widget does some backend requests which wait for max sync ID processed
|
||||
appContext.tabManager.getActiveContext().setNote(nodeCtx.nextNotePath).then(() => {
|
||||
const newActiveNode = this.getActiveNode();
|
||||
|
||||
// return focus if the previously active node was also focused
|
||||
if (newActiveNode && nodeCtx.activeNodeFocused) {
|
||||
newActiveNode.setFocus(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeNodeFocused) {
|
||||
// needed by Firefox: https://github.com/zadam/trilium/issues/1865
|
||||
this.tree.$container.focus();
|
||||
}
|
||||
|
||||
await node.setActive(true, {noEvents: true, noFocus: !activeNodeFocused});
|
||||
}
|
||||
|
||||
sortChildren(node) {
|
||||
|
||||
@@ -30,9 +30,14 @@ const TPL = `
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.title-bar-buttons .top-btn.active{
|
||||
background-color:var(--accented-background-color);
|
||||
}
|
||||
|
||||
.title-bar-buttons .btn.focus, .title-bar-buttons .btn:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- divs act as a hitbox for the buttons, making them clickable on corners -->
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import utils from "../../services/utils.js";
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import libraryLoader from "../../services/library_loader.js";
|
||||
import contextMenu from "../../menus/context_menu.js";
|
||||
import imageContextMenuService from "../../menus/image_context_menu.js";
|
||||
import imageService from "../../services/image.js";
|
||||
|
||||
const TPL = `
|
||||
@@ -55,36 +55,7 @@ class ImageTypeWidget extends TypeWidget {
|
||||
});
|
||||
});
|
||||
|
||||
if (utils.isElectron()) {
|
||||
// for browser, we want to let the native menu
|
||||
this.$imageView.on('contextmenu', e => {
|
||||
e.preventDefault();
|
||||
|
||||
contextMenu.show({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
{
|
||||
title: "Copy reference to clipboard",
|
||||
command: "copyImageReferenceToClipboard",
|
||||
uiIcon: "bx bx-empty"
|
||||
},
|
||||
{title: "Copy image to clipboard", command: "copyImageToClipboard", uiIcon: "bx bx-empty"},
|
||||
],
|
||||
selectMenuItemHandler: ({command}) => {
|
||||
if (command === 'copyImageReferenceToClipboard') {
|
||||
imageService.copyImageReferenceToClipboard(this.$imageWrapper);
|
||||
} else if (command === 'copyImageToClipboard') {
|
||||
const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
|
||||
utils.dynamicRequire('electron');
|
||||
webContents.copyImageAt(e.pageX, e.pageY);
|
||||
} else {
|
||||
throw new Error(`Unrecognized command '${command}'`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
imageContextMenuService.setupContextMenu(this.$imageView);
|
||||
|
||||
super.doRender();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ const TPL = `
|
||||
<h5>Highlights List visibility</h5>
|
||||
|
||||
<p>You can hide the highlights widget per-note by adding a <code>#hideHighlightWidget</code> label.</p>
|
||||
|
||||
<p>You can configure a keyboard shortcut for quickly toggling the right pane (including Highlights) in the Options -> Shortcuts (name "toggleRightPane").</p>
|
||||
</div>`;
|
||||
|
||||
export default class HighlightsListOptions extends OptionsWidget {
|
||||
|
||||
@@ -11,6 +11,8 @@ const TPL = `
|
||||
</div>
|
||||
|
||||
<p>You can also use this option to effectively disable TOC by setting a very high number.</p>
|
||||
|
||||
<p>You can configure a keyboard shortcut for quickly toggling the right pane (including TOC) in the Options -> Shortcuts (name "toggleRightPane").</p>
|
||||
</div>`;
|
||||
|
||||
export default class TableOfContentsOptions extends OptionsWidget {
|
||||
|
||||
@@ -9,14 +9,9 @@
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/vX/images/app-icons/ios/apple-touch-icon.png",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/assets/vX/images/app-icons/png/512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
"src": "favicon.ico",
|
||||
"sizes": "180x180 512x512",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ body {
|
||||
|
||||
--ck-color-base-text: var(--main-text-color);
|
||||
--ck-color-base-foreground: var(--accented-background-color);
|
||||
--ck-color-base-background: var(--main-background-color);
|
||||
--ck-color-focus-border: var(--main-border-color);
|
||||
--ck-color-text: var(--main-text-color);
|
||||
--ck-color-shadow-drop: var(--main-background-color);
|
||||
|
||||
@@ -88,3 +88,7 @@ body .CodeMirror {
|
||||
.excalidraw.theme--dark {
|
||||
--theme-filter: invert(80%) hue-rotate(180deg) !important;
|
||||
}
|
||||
|
||||
body .todo-list input[type="checkbox"]:not(:checked):before {
|
||||
border-color: var(--muted-text-color) !important;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,13 @@ function getLightAnonymizationScript() {
|
||||
SELECT blobId FROM notes WHERE mime IN ('application/javascript;env=backend', 'application/javascript;env=frontend')
|
||||
UNION ALL
|
||||
SELECT blobId FROM revisions WHERE mime IN ('application/javascript;env=backend', 'application/javascript;env=frontend')
|
||||
);`;
|
||||
);
|
||||
|
||||
UPDATE options SET value = 'anonymized' WHERE name IN
|
||||
('documentId', 'documentSecret', 'encryptedDataKey',
|
||||
'passwordVerificationHash', 'passwordVerificationSalt',
|
||||
'passwordDerivedKeySalt', 'username', 'syncServerHost', 'syncProxy')
|
||||
AND value != '';`;
|
||||
}
|
||||
|
||||
async function createAnonymizedCopy(type) {
|
||||
|
||||
@@ -4,8 +4,8 @@ const build = require('./build.js');
|
||||
const packageJson = require('../../package.json');
|
||||
const {TRILIUM_DATA_DIR} = require('./data_dir.js');
|
||||
|
||||
const APP_DB_VERSION = 227;
|
||||
const SYNC_VERSION = 31;
|
||||
const APP_DB_VERSION = 228;
|
||||
const SYNC_VERSION = 32;
|
||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -43,12 +43,16 @@ const optionsService = require('./options.js');
|
||||
*/
|
||||
function BackendScriptApi(currentNote, apiParams) {
|
||||
/**
|
||||
* Note where the script started executing
|
||||
* Note where the script started executing (entrypoint).
|
||||
* As an analogy, in C this would be the file which contains the main() function of the current process.
|
||||
* @type {BNote}
|
||||
*/
|
||||
this.startNote = apiParams.startNote;
|
||||
/**
|
||||
* Note where the script is currently executing. Don't mix this up with the concept of active note
|
||||
* Note where the script is currently executing. This comes into play when your script is spread in multiple code
|
||||
* notes, the script starts in "startNote", but then through function calls may jump into another note (currentNote).
|
||||
* A similar concept in C would be __FILE__
|
||||
* Don't mix this up with the concept of active note.
|
||||
* @type {BNote}
|
||||
*/
|
||||
this.currentNote = currentNote;
|
||||
@@ -288,7 +292,7 @@ function BackendScriptApi(currentNote, apiParams) {
|
||||
* @param {string} params.parentNoteId
|
||||
* @param {string} params.title
|
||||
* @param {string|Buffer} params.content
|
||||
* @param {NoteType} params.type - text, code, file, image, search, book, relationMap, canvas
|
||||
* @param {NoteType} params.type - text, code, file, image, search, book, relationMap, canvas, webView
|
||||
* @param {string} [params.mime] - value is derived from default mimes for type
|
||||
* @param {boolean} [params.isProtected=false]
|
||||
* @param {boolean} [params.isExpanded=false]
|
||||
|
||||
@@ -5,12 +5,14 @@ const utils = require('./utils.js');
|
||||
|
||||
function getBlobPojo(entityName, entityId) {
|
||||
const entity = becca.getEntity(entityName, entityId);
|
||||
|
||||
if (!entity) {
|
||||
throw new NotFoundError(`Entity ${entityName} '${entityId}' was not found.`);
|
||||
}
|
||||
|
||||
const blob = becca.getBlob(entity);
|
||||
if (!blob) {
|
||||
throw new NotFoundError(`Blob ${entity.blobId} for ${entityName} '${entityId}' was not found.`);
|
||||
}
|
||||
|
||||
const pojo = blob.getPojo();
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2023-12-07T00:03:59+01:00", buildRevision: "2e23c521c356c2305124f5df0f474532fa5f34ce" };
|
||||
module.exports = { buildDate:"2024-03-03T06:58:18+01:00", buildRevision: "0ad337c8e806ba84d48d7b97aa46df52d9f236a8" };
|
||||
|
||||
@@ -48,6 +48,14 @@ function isEntityEventsDisabled() {
|
||||
return !!namespace.get('disableEntityEvents');
|
||||
}
|
||||
|
||||
function setMigrationRunning(running) {
|
||||
namespace.set('migrationRunning', !!running);
|
||||
}
|
||||
|
||||
function isMigrationRunning() {
|
||||
return !!namespace.get('migrationRunning');
|
||||
}
|
||||
|
||||
function disableSlowQueryLogging(disable) {
|
||||
namespace.set('disableSlowQueryLogging', disable);
|
||||
}
|
||||
@@ -102,5 +110,7 @@ module.exports = {
|
||||
putEntityChange,
|
||||
ignoreEntityChangeIds,
|
||||
disableSlowQueryLogging,
|
||||
isSlowQueryLoggingDisabled
|
||||
isSlowQueryLoggingDisabled,
|
||||
setMigrationRunning,
|
||||
isMigrationRunning
|
||||
};
|
||||
|
||||
@@ -17,6 +17,9 @@ const {sanitizeAttributeName} = require('./sanitize_attribute_name.js');
|
||||
const noteTypes = require('../services/note_types.js').getNoteTypeNames();
|
||||
|
||||
class ConsistencyChecks {
|
||||
/**
|
||||
* @param autoFix - automatically fix all encountered problems. False is only for debugging during development (fail fast)
|
||||
*/
|
||||
constructor(autoFix) {
|
||||
this.autoFix = autoFix;
|
||||
this.unrecoveredConsistencyErrors = false;
|
||||
|
||||
@@ -37,6 +37,8 @@ function eraseNotes(noteIdsToErase) {
|
||||
function setEntityChangesAsErased(entityChanges) {
|
||||
for (const ec of entityChanges) {
|
||||
ec.isErased = true;
|
||||
// we're not changing hash here, not sure if good or not
|
||||
// content hash check takes isErased flag into account, though
|
||||
ec.utcDateChanged = dateUtils.utcNowDateTime();
|
||||
|
||||
entityChangesService.putEntityChangeWithForcedChange(ec);
|
||||
|
||||
@@ -181,6 +181,8 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
|
||||
|
||||
noteIdToMeta[note.noteId] = meta;
|
||||
|
||||
// sort children for having a stable / reproducible export format
|
||||
note.sortChildren();
|
||||
const childBranches = note.getChildBranches()
|
||||
.filter(branch => branch.noteId !== '_hidden');
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
|
||||
{
|
||||
actionName: "reopenLastTab",
|
||||
defaultShortcuts: isElectron ? ["CommandOrControl+Shift+T"] : [],
|
||||
description: "Repoens the last closed tab",
|
||||
description: "Reopens the last closed tab",
|
||||
scope: "window"
|
||||
},
|
||||
{
|
||||
@@ -291,7 +291,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
|
||||
{
|
||||
actionName: "eigthTab",
|
||||
defaultShortcuts: ["CommandOrControl+8"],
|
||||
description: "Activates the eigth tab in the list",
|
||||
description: "Activates the eighth tab in the list",
|
||||
scope: "window"
|
||||
},
|
||||
{
|
||||
@@ -302,7 +302,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
|
||||
},
|
||||
{
|
||||
actionName: "lastTab",
|
||||
defaultShortcuts: ["CommandOrControl+0"],
|
||||
defaultShortcuts: [],
|
||||
description: "Activates the last tab in the list",
|
||||
scope: "window"
|
||||
},
|
||||
@@ -494,9 +494,16 @@ const DEFAULT_KEYBOARD_ACTIONS = [
|
||||
separator: "Other"
|
||||
},
|
||||
|
||||
{
|
||||
actionName: "toggleRightPane",
|
||||
defaultShortcuts: [],
|
||||
description: "Toggle the display of the right pane, which includes Table of Contents and Highlights",
|
||||
scope: "window"
|
||||
},
|
||||
{
|
||||
actionName: "printActiveNote",
|
||||
defaultShortcuts: [],
|
||||
description: "Print active note",
|
||||
scope: "window"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,12 +5,13 @@ const log = require('./log.js');
|
||||
const utils = require('./utils.js');
|
||||
const resourceDir = require('./resource_dir.js');
|
||||
const appInfo = require('./app_info.js');
|
||||
const cls = require('./cls.js');
|
||||
|
||||
async function migrate() {
|
||||
const currentDbVersion = getDbVersion();
|
||||
|
||||
if (currentDbVersion < 214) {
|
||||
log.error("Direct migration from your current version is not supported. Please upgrade to the latest v0.60.X first and only then to this version.");
|
||||
log.error("Direct migration from your current version is not supported. Please upgrade to the latest v0.60.4 first and only then to this version.");
|
||||
|
||||
utils.crash();
|
||||
return;
|
||||
@@ -18,7 +19,7 @@ async function migrate() {
|
||||
|
||||
// backup before attempting migration
|
||||
await backupService.backupNow(
|
||||
// creating a special backup for versions 0.60.X, the changes in 0.61 are major.
|
||||
// creating a special backup for version 0.60.4, the changes in 0.61 are major.
|
||||
currentDbVersion === 214
|
||||
? `before-migration-v060`
|
||||
: 'before-migration'
|
||||
@@ -51,6 +52,9 @@ async function migrate() {
|
||||
// all migrations are executed in one transaction - upgrade either succeeds, or the user can stay at the old version
|
||||
// otherwise if half of the migrations succeed, user can't use any version - DB is too "new" for the old app,
|
||||
// and too old for the new app version.
|
||||
|
||||
cls.setMigrationRunning(true);
|
||||
|
||||
sql.transactional(() => {
|
||||
for (const mig of migrations) {
|
||||
try {
|
||||
@@ -73,8 +77,11 @@ async function migrate() {
|
||||
}
|
||||
});
|
||||
|
||||
log.info("VACUUMing database, this might take a while ...");
|
||||
sql.execute("VACUUM");
|
||||
if (currentDbVersion === 214) {
|
||||
// special VACUUM after the big migration
|
||||
log.info("VACUUMing database, this might take a while ...");
|
||||
sql.execute("VACUUM");
|
||||
}
|
||||
}
|
||||
|
||||
function executeMigration(mig) {
|
||||
|
||||
@@ -471,6 +471,8 @@ function findRelationMapLinks(content, foundLinks) {
|
||||
const imageUrlToAttachmentIdMapping = {};
|
||||
|
||||
async function downloadImage(noteId, imageUrl) {
|
||||
const unescapedUrl = utils.unescapeHtml(imageUrl);
|
||||
|
||||
try {
|
||||
let imageBuffer;
|
||||
|
||||
@@ -487,14 +489,14 @@ async function downloadImage(noteId, imageUrl) {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
imageBuffer = await request.getImage(imageUrl);
|
||||
imageBuffer = await request.getImage(unescapedUrl);
|
||||
}
|
||||
|
||||
const parsedUrl = url.parse(imageUrl);
|
||||
const parsedUrl = url.parse(unescapedUrl);
|
||||
const title = path.basename(parsedUrl.pathname);
|
||||
|
||||
const imageService = require('../services/image.js');
|
||||
const {attachment} = imageService.saveImageToAttachment(noteId, imageBuffer, title, true, true);
|
||||
const attachment = imageService.saveImageToAttachment(noteId, imageBuffer, title, true, true);
|
||||
|
||||
imageUrlToAttachmentIdMapping[imageUrl] = attachment.attachmentId;
|
||||
|
||||
@@ -511,7 +513,7 @@ const downloadImagePromises = {};
|
||||
function replaceUrl(content, url, attachment) {
|
||||
const quotedUrl = utils.quoteRegex(url);
|
||||
|
||||
return content.replace(new RegExp(`\\s+src=[\"']${quotedUrl}[\"']`, "ig"), ` src="api/attachments/${encodeURIComponent(attachment.title)}/image"`);
|
||||
return content.replace(new RegExp(`\\s+src=[\"']${quotedUrl}[\"']`, "ig"), ` src="api/attachments/${attachment.attachmentId}/image/${encodeURIComponent(attachment.title)}"`);
|
||||
}
|
||||
|
||||
function downloadImages(noteId, content) {
|
||||
@@ -636,6 +638,10 @@ function saveAttachments(note, content) {
|
||||
content = `${content.substr(0, attachmentMatch.index)}<a class="reference-link" href="#root/${note.noteId}?viewMode=attachments&attachmentId=${attachment.attachmentId}">${title}</a>${content.substr(attachmentMatch.index + attachmentMatch[0].length)}`;
|
||||
}
|
||||
|
||||
// removing absolute references to server to keep it working between instances,
|
||||
// we also omit / at the beginning to keep the paths relative
|
||||
content = content.replace(/src="[^"]*\/api\/attachments\//g, 'src="api/attachments/');
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -889,6 +895,11 @@ function scanForLinks(note, content) {
|
||||
* Things which have to be executed after updating content, but asynchronously (separate transaction)
|
||||
*/
|
||||
async function asyncPostProcessContent(note, content) {
|
||||
if (cls.isMigrationRunning()) {
|
||||
// this is rarely needed for migrations, but can cause trouble by e.g. triggering downloads
|
||||
return;
|
||||
}
|
||||
|
||||
if (note.hasStringContent() && !utils.isString(content)) {
|
||||
content = content.toString();
|
||||
}
|
||||
|
||||
@@ -63,10 +63,15 @@ function exec(opts) {
|
||||
}
|
||||
|
||||
let responseStr = '';
|
||||
let chunks = [];
|
||||
|
||||
response.on('data', chunk => responseStr += chunk);
|
||||
response.on('data', chunk => chunks.push(chunk));
|
||||
|
||||
response.on('end', () => {
|
||||
// use Buffer instead of string concatenation to avoid implicit decoding for each chunk
|
||||
// decode the entire data chunks explicitly as utf-8
|
||||
responseStr = Buffer.concat(chunks).toString('utf-8')
|
||||
|
||||
if ([200, 201, 204].includes(response.statusCode)) {
|
||||
try {
|
||||
const jsonObj = responseStr.trim() ? JSON.parse(responseStr) : null;
|
||||
|
||||
@@ -111,11 +111,7 @@ class NoteContentFulltextExp extends Expression {
|
||||
|
||||
if (type === 'text' && mime === 'text/html') {
|
||||
if (!this.raw && content.length < 20000) { // striptags is slow for very large notes
|
||||
// allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412
|
||||
content = striptags(content, ['a'], ' ');
|
||||
|
||||
// at least the closing tag can be easily stripped
|
||||
content = content.replace(/<\/a>/ig, "");
|
||||
content = this.stripTags(content);
|
||||
}
|
||||
|
||||
content = content.replace(/ /g, ' ');
|
||||
@@ -123,6 +119,23 @@ class NoteContentFulltextExp extends Expression {
|
||||
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
stripTags(content) {
|
||||
// we want to allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412
|
||||
// we want to insert space in place of block tags (because they imply text separation)
|
||||
// but we don't want to insert text for typical formatting inline tags which can occur within one word
|
||||
const linkTag = 'a';
|
||||
const inlineFormattingTags = ['b', 'strong', 'em', 'i', 'span', 'big', 'small', 'font', 'sub', 'sup'];
|
||||
|
||||
// replace tags which imply text separation with a space
|
||||
content = striptags(content, [linkTag, ...inlineFormattingTags], ' ');
|
||||
|
||||
// replace the inline formatting tags (but not links) without a space
|
||||
content = striptags(content, [linkTag], '');
|
||||
|
||||
// at least the closing link tag can be easily stripped
|
||||
return content.replace(/<\/a>/ig, "");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NoteContentFulltextExp;
|
||||
|
||||
@@ -73,7 +73,6 @@ function updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext
|
||||
if (localEC?.isErased) {
|
||||
eraseEntity(remoteEC); // make sure it's erased anyway
|
||||
updateContext.alreadyErased++;
|
||||
return false; // we won't save entitychange in this case
|
||||
} else {
|
||||
eraseEntity(remoteEC);
|
||||
updateContext.erased++;
|
||||
@@ -91,12 +90,17 @@ function updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext
|
||||
updateContext.updated[remoteEC.entityName].push(remoteEC.entityId);
|
||||
}
|
||||
|
||||
if (!localEC || localEC.utcDateChanged < remoteEC.utcDateChanged || localEC.hash !== remoteEC.hash) {
|
||||
if (!localEC
|
||||
|| localEC.utcDateChanged < remoteEC.utcDateChanged
|
||||
|| localEC.hash !== remoteEC.hash
|
||||
|| localEC.isErased !== remoteEC.isErased
|
||||
) {
|
||||
entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (localEC.hash !== remoteEC.hash && localEC.utcDateChanged > remoteEC.utcDateChanged) {
|
||||
} else if ((localEC.hash !== remoteEC.hash || localEC.isErased !== remoteEC.isErased)
|
||||
&& localEC.utcDateChanged > remoteEC.utcDateChanged) {
|
||||
// the change on our side is newer than on the other side, so the other side should update
|
||||
entityChangesService.putEntityChangeForOtherInstances(localEC);
|
||||
|
||||
@@ -148,7 +152,7 @@ function eraseEntity(entityChange) {
|
||||
];
|
||||
|
||||
if (!entityNames.includes(entityName)) {
|
||||
log.error(`Cannot erase entity '${entityName}', id '${entityId}'.`);
|
||||
log.error(`Cannot erase ${entityName} '${entityId}'.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ function hashedBlobId(content) {
|
||||
|
||||
// we don't want such + and / in the IDs
|
||||
const kindaBase62Hash = base64Hash
|
||||
.replace('+', 'X')
|
||||
.replace('/', 'Y');
|
||||
.replaceAll('+', 'X')
|
||||
.replaceAll('/', 'Y');
|
||||
|
||||
// 20 characters of base62 gives us ~120 bit of entropy which is plenty enough
|
||||
return kindaBase62Hash.substr(0, 20);
|
||||
|
||||
@@ -105,10 +105,10 @@ function renderText(result, note) {
|
||||
|
||||
if (result.content.includes(`<span class="math-tex">`)) {
|
||||
result.header += `
|
||||
<script src="../../${assetPath}/libraries/katex/katex.min.js"></script>
|
||||
<link rel="stylesheet" href="../../${assetPath}/libraries/katex/katex.min.css">
|
||||
<script src="../../${assetPath}/libraries/katex/auto-render.min.js"></script>
|
||||
<script src="../../${assetPath}/libraries/katex/mhchem.min.js"></script>
|
||||
<script src="../../${assetPath}/node_modules/katex/dist/katex.min.js"></script>
|
||||
<link rel="stylesheet" href="../../${assetPath}/node_modules/katex/dist/katex.min.css">
|
||||
<script src="../../${assetPath}/node_modules/katex/dist/contrib/auto-render.min.js"></script>
|
||||
<script src="../../${assetPath}/node_modules/katex/dist/contrib/mhchem.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
renderMathInElement(document.getElementById('content'));
|
||||
@@ -137,7 +137,7 @@ function renderCode(result) {
|
||||
|
||||
function renderMermaid(result, note) {
|
||||
result.content = `
|
||||
<img src="api/images/${note.noteId}/${note.escapedTitle}?${note.utcDateModified}">
|
||||
<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">
|
||||
<hr>
|
||||
<details>
|
||||
<summary>Chart source</summary>
|
||||
@@ -146,7 +146,7 @@ function renderMermaid(result, note) {
|
||||
}
|
||||
|
||||
function renderImage(result, note) {
|
||||
result.content = `<img src="api/images/${note.noteId}/${note.escapedTitle}?${note.utcDateModified}">`;
|
||||
result.content = `<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">`;
|
||||
}
|
||||
|
||||
function renderFile(note, result) {
|
||||
|
||||
@@ -490,6 +490,10 @@ class SNote extends AbstractShacaEntity {
|
||||
return escape(this.title);
|
||||
}
|
||||
|
||||
get encodedTitle() {
|
||||
return encodeURIComponent(this.title);
|
||||
}
|
||||
|
||||
getPojo() {
|
||||
return {
|
||||
noteId: this.noteId,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
|
||||
<title>Trilium Notes</title>
|
||||
</head>
|
||||
<body class="desktop heading-style-<%= headingStyle %>">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<meta name="theme-color" content="#fff">
|
||||
<title>Trilium Notes</title>
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
|
||||
|
||||
<style>
|
||||
.lds-roller {
|
||||
|
||||
Reference in New Issue
Block a user