Compare commits

...

29 Commits

Author SHA1 Message Date
zadam
5a85fe92aa release 0.49.2-beta 2022-01-02 22:43:30 +01:00
zadam
feffd57f24 added support for #pageUrl into shared notes 2022-01-02 22:43:00 +01:00
zadam
faf81ae056 fix closing new window, closes #2502 2022-01-02 21:35:02 +01:00
zadam
003fec4b11 fix interrupted initial sync 2022-01-02 21:20:56 +01:00
zadam
5ecb603e86 sharing fixes 2022-01-01 22:32:38 +01:00
zadam
1fed71a92e shaca now loads attributes, added favicon and shareJs 2022-01-01 13:23:09 +01:00
zadam
dad82ea4e8 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	package.json
#	src/services/build.js
2021-12-31 20:16:31 +01:00
zadam
067251861d Merge remote-tracking branch 'origin/stable' into stable 2021-12-30 21:06:52 +01:00
zadam
6bc8773d5f handle OPML with empty content, fixes #2495 2021-12-30 21:06:45 +01:00
Matt
a910034c96 Fix math rendering (#2487) 2021-12-28 20:12:02 +01:00
zadam
257cc66f62 ignore missing image notes when downloading/replacing images, #2486 2021-12-28 20:08:17 +01:00
zadam
00f24bdb63 share improvements 2021-12-28 13:57:37 +01:00
zadam
fada3fe623 share improvements 2021-12-28 13:40:51 +01:00
zadam
d97e454463 allow cloning into notes, not just branches, fixes #2484 2021-12-27 23:39:46 +01:00
zadam
3128a7d62f various tweaks to shared notes 2021-12-27 20:48:14 +01:00
zadam
b8fe9a41db fix consistency checks 2021-12-27 13:37:51 +01:00
Matt
8366a94bde Various share page improvements (#2471)
* Add clientside mermaid chart rendering

Merry Christmas :)

* Add katex math rendering client-side

* Update page.ejs

* Revert (wrong branch)

* Add children nodes to all notes under hr

* Add parent note button

* Add note type in child note info

* Fix parent, relative paths

* Add code rendering, fix space in HTML class
2021-12-27 13:27:00 +01:00
zadam
f56123b864 fix missing branch in case of out-of-order clones, closes #2464 2021-12-26 23:31:54 +01:00
DHMike57
ae951bfe23 Add option for vim keymap in codemirror (#2475) 2021-12-26 13:24:18 +01:00
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
265401775b release 0.48.9 2021-12-22 22:39:24 +01:00
46 changed files with 6196 additions and 162 deletions

5734
libraries/codemirror/keymap/vim.js vendored Normal file

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.49.0-beta",
"version": "0.49.2-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {

View File

@@ -1114,7 +1114,7 @@ class Note extends AbstractEntity {
const branch = this.becca.getNote(parentNoteId).getParentBranches()[0];
return cloningService.cloneNoteToParent(this.noteId, branch.branchId);
return cloningService.cloneNoteToBranch(this.noteId, branch.branchId);
}
decrypt() {

View File

@@ -48,7 +48,7 @@ async function cloneNotesTo(notePath) {
const targetBranchId = await froca.getBranchId(parentNoteId, noteId);
for (const cloneNoteId of clonedNoteIds) {
await branchService.cloneNoteTo(cloneNoteId, targetBranchId, $clonePrefix.val());
await branchService.cloneNoteToBranch(cloneNoteId, targetBranchId, $clonePrefix.val());
const clonedNote = await froca.getNote(cloneNoteId);
const targetNote = await froca.getBranch(targetBranchId).getNote();

View File

@@ -1,7 +1,15 @@
import mimeTypesService from "../../services/mime_types.js";
import options from "../../services/options.js";
import server from "../../services/server.js";
import toastService from "../../services/toast.js";
import utils from "../../services/utils.js";
const TPL = `
<h4>Use vim keybindings in CodeNotes (no ex mode)</h4>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="vim-keymap-enabled">
<label class="custom-control-label" for="vim-keymap-enabled">Enable Vim Keybindings</label>
</div>
<h4>Available MIME types in the dropdown</h4>
<ul id="options-mime-types" style="max-height: 500px; overflow: auto; list-style-type: none;"></ul>`;
@@ -10,12 +18,18 @@ export default class CodeNotesOptions {
constructor() {
$("#options-code-notes").html(TPL);
this.$vimKeymapEnabled = $("#vim-keymap-enabled");
this.$vimKeymapEnabled.on('change', () => {
const opts = { 'vimKeymapEnabled': this.$vimKeymapEnabled.is(":checked") ? "true" : "false" };
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
return false;
});
this.$mimeTypes = $("#options-mime-types");
}
async optionsLoaded() {
async optionsLoaded(options) {
this.$mimeTypes.empty();
this.$vimKeymapEnabled.prop("checked", options['vimKeymapEnabled'] === 'true');
let idCtr = 1;
for (const mimeType of await mimeTypesService.getMimeTypes()) {
@@ -45,4 +59,4 @@ export default class CodeNotesOptions {
mimeTypesService.loadMimeTypes();
}
}
}

View File

@@ -196,8 +196,18 @@ ws.subscribeToMessages(async message => {
}
});
async function cloneNoteTo(childNoteId, parentBranchId, prefix) {
const resp = await server.put(`notes/${childNoteId}/clone-to/${parentBranchId}`, {
async function cloneNoteToBranch(childNoteId, parentBranchId, prefix) {
const resp = await server.put(`notes/${childNoteId}/clone-to-branch/${parentBranchId}`, {
prefix: prefix
});
if (!resp.success) {
alert(resp.message);
}
}
async function cloneNoteToNote(childNoteId, parentNoteId, prefix) {
const resp = await server.put(`notes/${childNoteId}/clone-to-note/${parentNoteId}`, {
prefix: prefix
});
@@ -222,5 +232,6 @@ export default {
deleteNotes,
moveNodeUpInHierarchy,
cloneNoteAfter,
cloneNoteTo
cloneNoteToBranch,
cloneNoteToNote,
};

View File

@@ -51,7 +51,7 @@ async function pasteInto(parentBranchId) {
for (const clipboardBranch of clipboardBranches) {
const clipboardNote = await clipboardBranch.getNote();
await branchService.cloneNoteTo(clipboardNote.noteId, parentBranchId);
await branchService.cloneNoteToBranch(clipboardNote.noteId, parentBranchId);
}
// copy will keep clipboardBranchIds and clipboardMode so it's possible to paste into multiple places

View File

@@ -10,6 +10,7 @@ const CODE_MIRROR = {
"libraries/codemirror/addon/edit/matchtags.js",
"libraries/codemirror/addon/search/match-highlighter.js",
"libraries/codemirror/mode/meta.js",
"libraries/codemirror/keymap/vim.js",
"libraries/codemirror/addon/lint/lint.js",
"libraries/codemirror/addon/lint/eslint.js"
],

21
src/public/app/share.js Normal file
View File

@@ -0,0 +1,21 @@
/**
* Fetch note with given ID from backend
*
* @param noteId of the given note to be fetched. If falsy, fetches current note.
*/
async function fetchNote(noteId = null) {
if (!noteId) {
noteId = document.body.getAttribute("data-note-id");
}
const resp = await fetch(`api/notes/${noteId}`);
return await resp.json();
}
document.addEventListener('DOMContentLoaded', () => {
const toggleMenuButton = document.getElementById('toggleMenuButton');
const layout = document.getElementById('layout');
toggleMenuButton.addEventListener('click', () => layout.classList.toggle('showMenu'));
}, false);

View File

@@ -226,6 +226,8 @@ const ATTR_HELP = {
"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",
"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.",
"shareJs": "JavaScript note which will be injected into the share page. JS note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree'.",
"shareFavicon": "Favicon note to be set in the shared page. Typically you want to set it to share root and make it inheritable. Favicon note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree'.",
}
};

View File

@@ -103,17 +103,19 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
async refreshWithNote(note) {
this.clearItems();
const targetRelationCount = note.getTargetRelations().length;
if (targetRelationCount === 0) {
// 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;
}
else {
this.$ticker.show();
this.$count.text(
`${targetRelationCount} backlink`
+ (targetRelationCount === 1 ? '' : 's')
);
}
this.$ticker.show();
this.$count.text(
`${resp.count} backlink`
+ (resp.count === 1 ? '' : 's')
);
}
clearItems() {
@@ -136,18 +138,22 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
await froca.getNotes(backlinks.map(bl => bl.noteId)); // prefetch all
for (const backlink of backlinks) {
this.$items.append(await linkService.createNoteLink(backlink.noteId, {
const $item = $("<div>");
$item.append(await linkService.createNoteLink(backlink.noteId, {
showNoteIcon: true,
showNotePath: true,
showTooltip: false
}));
if (backlink.relationName) {
this.$items.append($("<p>").text("relation: " + backlink.relationName));
$item.append($("<p>").text("relation: " + backlink.relationName));
}
else {
this.$items.append(...backlink.excerpts);
$item.append(...backlink.excerpts);
}
this.$items.append($item);
}
}
}

View File

@@ -18,7 +18,7 @@ const TPL = `
export default class SharedInfoWidget extends NoteContextAwareWidget {
isEnabled() {
return super.isEnabled() && this.note.hasAncestor('share');
return super.isEnabled() && this.noteId !== 'share' && this.note.hasAncestor('share');
}
doRender() {

View File

@@ -4,6 +4,10 @@ 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();
@@ -18,7 +22,7 @@ export default class SharedSwitchWidget extends SwitchWidget {
}
switchOn() {
branchService.cloneNoteTo(this.noteId, 'share');
branchService.cloneNoteToNote(this.noteId, 'share');
}
async switchOff() {

View File

@@ -6,6 +6,7 @@ import ws from "../../services/ws.js";
import appContext from "../../services/app_context.js";
import toastService from "../../services/toast.js";
import treeService from "../../services/tree.js";
import options from "../../services/options.js";
const TPL = `
<div class="note-detail-code note-detail-printable">
@@ -14,20 +15,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 +28,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>
@@ -97,6 +95,7 @@ export default class EditableCodeTypeWidget extends TypeWidget {
viewportMargin: Infinity,
indentUnit: 4,
matchBrackets: true,
keyMap: options.is('vimKeymapEnabled') ? "vim": "default",
matchTags: {bothTags: true},
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false},
lint: true,

View File

@@ -32,15 +32,18 @@ body {
#main {
flex-basis: 0;
flex-grow: 3;
overflow: auto;
padding: 10px 20px 20px 20px;
}
#parentLink {
float: right;
margin-top: 20px;
}
#title {
margin: 0;
padding: 20px 20px 0 20px;
}
#content {
padding: 20px;
padding-top: 10px;
}
img {
@@ -52,7 +55,12 @@ pre {
word-wrap: anywhere;
}
#menuButton {
iframe.pdf-view {
width: 100%;
height: 800px;
}
#toggleMenuButton {
display: none;
position: fixed;
top: 8px;
@@ -67,23 +75,74 @@ pre {
cursor: pointer;
}
#menuButton::after {
#childLinks.grid ul {
list-style-type: none;
display: flex;
flex-wrap: wrap;
padding: 0;
}
#childLinks.grid ul li {
width: 180px;
height: 140px;
padding: 10px;
}
#childLinks.grid ul li a {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
border: 1px solid #ddd;
border-radius: 5px;
justify-content: center;
align-content: center;
text-align: center;
font-size: large;
}
#childLinks.grid ul li a:hover {
background: #eee;
}
#childLinks.list ul {
list-style-type: none;
display: inline-flex;
flex-wrap: wrap;
padding: 0;
margin-top: 5px;
}
#childLinks.list ul li {
margin-right: 20px;
}
#noteClippedFrom {
padding: 10px 0 10px 0;
margin: 20px 0 20px 0;
color: #666;
border: 1px solid #ddd;
border-left: 0;
border-right: 0;
}
#toggleMenuButton::after {
position: relative;
top: -2px;
left: 1px;
}
@media (max-width: 48em) {
#layout.navMenu #menu {
#layout.showMenu #menu {
display: block;
margin-top: 40px;
}
#menuButton {
#toggleMenuButton {
display: block;
}
#layout.navMenu #main {
#layout.showMenu #main {
display: none;
}
@@ -91,11 +150,11 @@ pre {
padding-left: 60px;
}
#layout.navMenu #menuButton::after {
#layout.showMenu #toggleMenuButton::after {
content: "«";
}
#menuButton::after {
#toggleMenuButton::after {
content: "»";
}

View File

@@ -2,11 +2,18 @@
const cloningService = require('../../services/cloning');
function cloneNoteToParent(req) {
function cloneNoteToBranch(req) {
const {noteId, parentBranchId} = req.params;
const {prefix} = req.body;
return cloningService.cloneNoteToParent(noteId, parentBranchId, prefix);
return cloningService.cloneNoteToBranch(noteId, parentBranchId, prefix);
}
function cloneNoteToNote(req) {
const {noteId, parentNoteId} = req.params;
const {prefix} = req.body;
return cloningService.cloneNoteToNote(noteId, parentNoteId, prefix);
}
function cloneNoteAfter(req) {
@@ -16,6 +23,7 @@ function cloneNoteAfter(req) {
}
module.exports = {
cloneNoteToParent,
cloneNoteToBranch,
cloneNoteToNote,
cloneNoteAfter
};

View File

@@ -302,6 +302,21 @@ function uploadModifiedFile(req) {
note.setContent(fileContent);
}
function getBacklinkCount(req) {
const {noteId} = req.params;
const note = becca.getNote(noteId);
if (!note) {
return [404, "Not found"];
}
else {
return {
count: note.getTargetRelations().length
};
}
}
module.exports = {
getNote,
updateNote,
@@ -316,5 +331,6 @@ module.exports = {
duplicateSubtree,
eraseDeletedNotesNow,
getDeleteNotesPreview,
uploadModifiedFile
uploadModifiedFile,
getBacklinkCount
};

View File

@@ -33,6 +33,7 @@ const ALLOWED_OPTIONS = new Set([
'similarNotesWidget',
'editedNotesWidget',
'calendarWidget',
'vimKeymapEnabled',
'codeNotesMimeTypes',
'spellCheckEnabled',
'spellCheckLanguageCode',

View File

@@ -220,6 +220,7 @@ function register(app) {
apiRoute(DELETE, '/api/notes/:noteId/revisions/:noteRevisionId', noteRevisionsApiRoute.eraseNoteRevision);
route(GET, '/api/notes/:noteId/revisions/:noteRevisionId/download', [auth.checkApiAuthOrElectron], noteRevisionsApiRoute.downloadNoteRevision);
apiRoute(PUT, '/api/notes/:noteId/restore-revision/:noteRevisionId', noteRevisionsApiRoute.restoreNoteRevision);
apiRoute(GET, '/api/notes/:noteId/backlink-count', notesApiRoute.getBacklinkCount);
apiRoute(POST, '/api/notes/relation-map', notesApiRoute.getRelationMap);
apiRoute(POST, '/api/notes/erase-deleted-notes-now', notesApiRoute.eraseDeletedNotesNow);
apiRoute(PUT, '/api/notes/:noteId/change-title', notesApiRoute.changeTitle);
@@ -228,7 +229,8 @@ function register(app) {
apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate);
apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentBranchId', cloningApiRoute.cloneNoteToParent);
apiRoute(PUT, '/api/notes/:noteId/clone-to-branch/:parentBranchId', cloningApiRoute.cloneNoteToBranch);
apiRoute(PUT, '/api/notes/:noteId/clone-to-note/:parentNoteId', cloningApiRoute.cloneNoteToNote);
apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter);
route(GET, '/api/notes/:branchId/export/:type/:format/:version/:taskId', [auth.checkApiAuthOrElectron], exportRoute.exportBranch);

View File

@@ -67,6 +67,8 @@ const BUILTIN_ATTRIBUTES = [
{ type: 'relation', name: 'widget', isDangerous: true },
{ type: 'relation', name: 'renderNote', isDangerous: true },
{ type: 'relation', name: 'shareCss', isDangerous: false },
{ type: 'relation', name: 'shareJs', isDangerous: false },
{ type: 'relation', name: 'shareFavicon', isDangerous: false },
];
/** @returns {Note[]} */

View File

@@ -1 +1 @@
module.exports = { buildDate:"2021-12-23T23:03:21+01:00", buildRevision: "f0217cae5eb4bdac12efe1d15bf26dc128e7f854" };
module.exports = { buildDate:"2022-01-02T22:43:30+01:00", buildRevision: "feffd57f240438d107c1ed1c1772545611a97dee" };

View File

@@ -10,20 +10,18 @@ const utils = require('./utils');
const becca = require("../becca/becca");
const beccaService = require("../becca/becca_service");
function cloneNoteToParent(noteId, parentBranchId, prefix) {
if (parentBranchId === 'share') {
function cloneNoteToNote(noteId, parentNoteId, prefix) {
if (parentNoteId === 'share') {
const specialNotesService = require('./special_notes');
// share root note is created lazily
specialNotesService.getShareRoot();
}
const parentBranch = becca.getBranch(parentBranchId);
if (isNoteDeleted(noteId) || isNoteDeleted(parentBranch.noteId)) {
if (isNoteDeleted(noteId) || isNoteDeleted(parentNoteId)) {
return { success: false, message: 'Note is deleted.' };
}
const validationResult = treeService.validateParentChild(parentBranch.noteId, noteId);
const validationResult = treeService.validateParentChild(parentNoteId, noteId);
if (!validationResult.success) {
return validationResult;
@@ -31,21 +29,33 @@ function cloneNoteToParent(noteId, parentBranchId, prefix) {
const branch = new Branch({
noteId: noteId,
parentNoteId: parentBranch.noteId,
parentNoteId: parentNoteId,
prefix: prefix,
isExpanded: 0
}).save();
parentBranch.isExpanded = true; // the new target should be expanded so it immediately shows up to the user
parentBranch.save();
return {
success: true,
branchId: branch.branchId,
notePath: beccaService.getNotePath(parentBranch.noteId).path + "/" + noteId
notePath: beccaService.getNotePath(parentNoteId).path + "/" + noteId
};
}
function cloneNoteToBranch(noteId, parentBranchId, prefix) {
const parentBranch = becca.getBranch(parentBranchId);
if (!parentBranch) {
return { success: false, message: `Parent branch ${parentBranchId} does not exist.` };
}
const ret = cloneNoteToNote(noteId, parentBranch.noteId, prefix);
parentBranch.isExpanded = true; // the new target should be expanded so it immediately shows up to the user
parentBranch.save();
return ret;
}
function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) {
if (isNoteDeleted(noteId) || isNoteDeleted(parentNoteId)) {
return { success: false, message: 'Note is deleted.' };
@@ -121,7 +131,8 @@ function isNoteDeleted(noteId) {
}
module.exports = {
cloneNoteToParent,
cloneNoteToBranch,
cloneNoteToNote,
ensureNoteIsPresentInParent,
ensureNoteIsAbsentFromParent,
toggleNoteInParent,

View File

@@ -259,7 +259,7 @@ class ConsistencyChecks {
WHERE noteId = ?
and parentNoteId = ?
and isDeleted = 0
ORDER BY utcDateCreated`, [noteId, parentNoteId]);
ORDER BY utcDateModified`, [noteId, parentNoteId]);
const branches = branchIds.map(branchId => becca.getBranch(branchId));

View File

@@ -54,7 +54,7 @@ function getYearNote(dateStr, rootNote) {
rootNote = getRootCalendarNote();
}
const yearStr = dateStr.substr(0, 4);
const yearStr = dateStr.trim().substr(0, 4);
let yearNote = attributeService.getNoteWithLabel(YEAR_LABEL, yearStr);
@@ -138,6 +138,8 @@ function getDateNoteTitle(rootNote, dayNumber, dateObj) {
/** @returns {Note} */
function getDateNote(dateStr) {
dateStr = dateStr.trim().substr(0, 10);
let dateNote = attributeService.getNoteWithLabel(DATE_LABEL, dateStr);
if (dateNote) {

View File

@@ -3,6 +3,10 @@ const sanitizeHtml = require('sanitize-html');
// intended mainly as protection against XSS via import
// secondarily it (partly) protects against "CSS takeover"
function sanitize(dirtyHtml) {
if (!dirtyHtml) {
return dirtyHtml;
}
// avoid H1 per https://github.com/zadam/trilium/issues/1552
// demote H1, and if that conflicts with existing H2, demote that, etc
const transformTags = {};

View File

@@ -51,7 +51,7 @@ async function importOpml(taskContext, fileBuffer, parentNote) {
throw new Error("Unrecognized OPML version " + opmlVersion);
}
content = htmlSanitizer.sanitize(content);
content = htmlSanitizer.sanitize(content || "");
const {note} = noteService.createNewNote({
parentNoteId,

View File

@@ -240,13 +240,15 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
}
if (noteMeta && noteMeta.isClone) {
new Branch({
noteId,
parentNoteId,
isExpanded: noteMeta.isExpanded,
prefix: noteMeta.prefix,
notePosition: noteMeta.notePosition
}).save();
if (!becca.getBranchFromChildAndParent(noteId, parentNoteId)) {
new Branch({
noteId,
parentNoteId,
isExpanded: noteMeta.isExpanded,
prefix: noteMeta.prefix,
notePosition: noteMeta.notePosition
}).save();
}
return;
}
@@ -365,6 +367,16 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
}
note.setContent(content);
if (!becca.getBranchFromChildAndParent(noteId, parentNoteId)) {
new Branch({
noteId,
parentNoteId,
isExpanded: noteMeta.isExpanded,
prefix: noteMeta.prefix,
notePosition: noteMeta.notePosition
}).save();
}
}
else {
({note} = noteService.createNewNote({

View File

@@ -360,7 +360,7 @@ function downloadImages(noteId, content) {
// which upon the download of all the images will update the note if the links have not been fixed before
sql.transactional(() => {
const imageNotes = becca.getNotes(Object.values(imageUrlToNoteIdMapping));
const imageNotes = becca.getNotes(Object.values(imageUrlToNoteIdMapping), true);
const origNote = becca.getNote(noteId);

View File

@@ -1,7 +1,16 @@
const becca = require('../becca/becca');
const sql = require("./sql.js");
function getOption(name) {
const option = require('../becca/becca').getOption(name);
let option;
if (becca.loaded) {
option = becca.getOption(name);
}
else {
// e.g. in initial sync becca is not loaded because DB is not initialized
option = sql.getRow("SELECT * FROM options WHERE name = ?", name);
}
if (!option) {
throw new Error(`Option "${name}" doesn't exist`);
@@ -39,12 +48,12 @@ function getOptionBool(name) {
}
function setOption(name, value) {
const option = becca.getOption(name);
if (value === true || value === false) {
value = value.toString();
}
const option = becca.getOption(name);
if (option) {
option.value = value;

View File

@@ -70,6 +70,7 @@ const defaultOptions = [
{ name: 'imageMaxWidthHeight', value: '2000', isSynced: true },
{ name: 'imageJpegQuality', value: '75', isSynced: true },
{ name: 'autoFixConsistencyIssues', value: 'true', isSynced: false },
{ name: 'vimKeymapEnabled', value: 'false', isSynced: false },
{ name: 'codeNotesMimeTypes', value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-sqlite;schema=trilium","text/x-swift","text/xml","text/x-yaml"]', isSynced: true },
{ name: 'leftPaneWidth', value: '25', isSynced: false },
{ name: 'leftPaneVisible', value: 'true', isSynced: false },

View File

@@ -371,7 +371,10 @@ function getLastSyncedPull() {
function setLastSyncedPull(entityChangeId) {
const lastSyncedPullOption = becca.getOption('lastSyncedPull');
lastSyncedPullOption.value = entityChangeId + '';
if (lastSyncedPullOption) { // might be null in initial sync when becca is not loaded
lastSyncedPullOption.value = entityChangeId + '';
}
// this way we avoid updating entity_changes which otherwise means that we've never pushed all entity_changes
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, 'lastSyncedPull']);
@@ -389,7 +392,10 @@ function setLastSyncedPush(entityChangeId) {
ws.setLastSyncedPush(entityChangeId);
const lastSyncedPushOption = becca.getOption('lastSyncedPush');
lastSyncedPushOption.value = entityChangeId + '';
if (lastSyncedPushOption) { // might be null in initial sync when becca is not loaded
lastSyncedPushOption.value = entityChangeId + '';
}
// this way we avoid updating entity_changes which otherwise means that we've never pushed all entity_changes
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, 'lastSyncedPush']);

View File

@@ -16,7 +16,10 @@ let mainWindow;
let setupWindow;
async function createExtraWindow(notePath, hoistedNoteId = 'root') {
const spellcheckEnabled = optionService.getOptionBool('spellCheckEnabled');
const {BrowserWindow} = require('electron');
const win = new BrowserWindow({
width: 1000,
height: 800,
@@ -25,7 +28,7 @@ async function createExtraWindow(notePath, hoistedNoteId = 'root') {
enableRemoteModule: true,
nodeIntegration: true,
contextIsolation: false,
spellcheck: optionService.getOptionBool('spellCheckEnabled')
spellcheck: spellcheckEnabled
},
frame: optionService.getOptionBool('nativeTitleBarVisible'),
icon: getIcon()
@@ -33,6 +36,8 @@ async function createExtraWindow(notePath, hoistedNoteId = 'root') {
win.setMenuBarVisibility(false);
win.loadURL('http://127.0.0.1:' + await port + '/?extra=1&extraHoistedNoteId=' + hoistedNoteId + '#' + notePath);
configureWebContents(win.webContents, spellcheckEnabled);
}
ipcMain.on('create-extra-window', (event, arg) => {
@@ -59,6 +64,7 @@ async function createMainWindow() {
height: mainWindowState.height,
title: 'Trilium Notes',
webPreferences: {
enableRemoteModule: true,
nodeIntegration: true,
contextIsolation: false,
spellcheck: spellcheckEnabled
@@ -73,8 +79,10 @@ async function createMainWindow() {
mainWindow.loadURL('http://127.0.0.1:' + await port);
mainWindow.on('closed', () => mainWindow = null);
const {webContents} = mainWindow;
configureWebContents(mainWindow.webContents, spellcheckEnabled);
}
function configureWebContents(webContents, spellcheckEnabled) {
require("@electron/remote/main").enable(webContents);
webContents.on('new-window', (e, url) => {

View File

@@ -1,43 +1,18 @@
const {JSDOM} = require("jsdom");
const NO_CONTENT = '<p>This note has no content.</p>';
const shaca = require("./shaca/shaca");
function getChildrenList(note) {
if (note.hasChildren()) {
const document = new JSDOM().window.document;
const ulEl = document.createElement("ul");
for (const childNote of note.getChildNotes()) {
const li = document.createElement("li");
const link = document.createElement("a");
link.appendChild(document.createTextNode(childNote.title));
link.setAttribute("href", childNote.noteId);
li.appendChild(link);
ulEl.appendChild(li);
}
return '<p>Child notes:</p>' + ulEl.outerHTML;
}
else {
return '';
}
}
function getContent(note) {
let content = note.getContent();
let header = '';
let isEmpty = false;
if (note.type === 'text') {
const document = new JSDOM(content || "").window.document;
const isEmpty = document.body.textContent.trim().length === 0
isEmpty = document.body.textContent.trim().length === 0
&& document.querySelectorAll("img").length === 0;
if (isEmpty) {
content = NO_CONTENT + getChildrenList(note);
}
else {
if (!isEmpty) {
for (const linkEl of document.querySelectorAll("a")) {
const href = linkEl.getAttribute("href");
@@ -49,6 +24,7 @@ function getContent(note) {
if (linkedNote) {
linkEl.setAttribute("href", linkedNote.shareId);
linkEl.classList.add("type-" + linkedNote.type);
}
else {
linkEl.removeAttribute("href");
@@ -57,11 +33,24 @@ function getContent(note) {
}
content = document.body.innerHTML;
if (content.includes(`<span class="math-tex">`)) {
header += `
<script src="../../libraries/katex/katex.min.js"></script>
<link rel="stylesheet" href="../../libraries/katex/katex.min.css">
<script src="../../libraries/katex/auto-render.min.js"></script>
<script src="../../libraries/katex/mhchem.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
renderMathInElement(document.getElementById('content'));
});
</script>`;
}
}
}
else if (note.type === 'code' || note.type === 'mermaid') {
else if (note.type === 'code') {
if (!content?.trim()) {
content = NO_CONTENT + getChildrenList(note);
isEmpty = true;
}
else {
const document = new JSDOM().window.document;
@@ -72,22 +61,45 @@ function getContent(note) {
content = preEl.outerHTML;
}
}
else if (note.type === 'mermaid') {
content = `
<div class="mermaid">${content}</div>
<hr>
<details>
<summary>Chart source</summary>
<pre>${content}</pre>
</details>`
header += `<script src="../../libraries/mermaid.min.js"></script>`;
}
else if (note.type === 'image') {
content = `<img src="api/images/${note.noteId}/${note.title}?${note.utcDateModified}">`;
}
else if (note.type === 'file') {
content = `<button type="button" onclick="location.href='api/notes/${note.noteId}/download'">Download file</button>`;
if (note.mime === 'application/pdf') {
content = `<iframe class="pdf-view" src="api/notes/${note.noteId}/view"></iframe>`
}
else {
content = `<button type="button" onclick="location.href='api/notes/${note.noteId}/download'">Download file</button>`;
}
}
else if (note.type === 'book') {
content = getChildrenList(note);
isEmpty = true;
}
else {
content = '<p>This note type cannot be displayed.</p>' + getChildrenList(note);
content = '<p>This note type cannot be displayed.</p>';
}
return content;
return {
header,
content,
isEmpty
};
}
module.exports = {
getContent
};

View File

@@ -29,13 +29,15 @@ function register(router) {
const note = shaca.aliasToNote[shareId] || shaca.notes[shareId];
if (note) {
const content = contentRenderer.getContent(note);
const {header, content, isEmpty} = contentRenderer.getContent(note);
const subRoot = getSharedSubTreeRoot(note);
res.render("share/page", {
note,
header,
content,
isEmpty,
subRoot
});
}
@@ -44,19 +46,15 @@ function register(router) {
}
});
router.get('/share/api/images/:noteId/:filename', (req, res, next) => {
const image = shaca.getNote(req.params.noteId);
router.get('/share/api/notes/:noteId', (req, res, next) => {
const {noteId} = req.params;
const note = shaca.getNote(noteId);
if (!image) {
return res.status(404).send("Not found");
}
else if (image.type !== 'image') {
return res.status(400).send("Requested note is not an image");
if (!note) {
return res.status(404).send(`Note ${noteId} not found`);
}
res.set('Content-Type', image.mime);
res.send(image.getContent());
res.json(note.getPojoWithAttributes());
});
router.get('/share/api/notes/:noteId/download', (req, res, next) => {
@@ -64,7 +62,7 @@ function register(router) {
const note = shaca.getNote(noteId);
if (!note) {
return res.status(404).send(`Not found`);
return res.status(404).send(`Note ${noteId} not found`);
}
const utils = require("../services/utils");
@@ -78,6 +76,36 @@ function register(router) {
res.send(note.getContent());
});
router.get('/share/api/images/:noteId/:filename', (req, res, next) => {
const image = shaca.getNote(req.params.noteId);
if (!image) {
return res.status(404).send(`Note ${noteId} not found`);
}
else if (image.type !== 'image') {
return res.status(400).send("Requested note is not an image");
}
res.set('Content-Type', image.mime);
res.send(image.getContent());
});
// used for PDF viewing
router.get('/share/api/notes/:noteId/view', (req, res, next) => {
const {noteId} = req.params;
const note = shaca.getNote(noteId);
if (!note) {
return res.status(404).send(`Note ${noteId} not found`);
}
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader('Content-Type', note.mime);
res.send(note.getContent());
});
}
module.exports = {

View File

@@ -89,6 +89,18 @@ class Attribute extends AbstractEntity {
return this.shaca.getNote(this.value);
}
getPojo() {
return {
attributeId: this.attributeId,
noteId: this.noteId,
type: this.type,
name: this.name,
position: this.position,
value: this.value,
isInheritable: this.isInheritable
};
}
}
module.exports = Attribute;

View File

@@ -410,6 +410,19 @@ class Note extends AbstractEntity {
return sharedAlias || this.noteId;
}
getPojoWithAttributes() {
return {
noteId: this.noteId,
title: this.title,
type: this.type,
mime: this.mime,
utcDateModified: this.utcDateModified,
attributes: this.getAttributes().map(attr => attr.getPojo()),
parentNoteIds: this.parents.map(parentNote => parentNote.noteId),
childNoteIds: this.children.map(child => child.noteId)
};
}
}
module.exports = Note;

View File

@@ -59,11 +59,7 @@ function load() {
SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified
FROM attributes
WHERE isDeleted = 0
AND noteId IN (${noteIdStr})
AND (
(type = 'label' AND name IN ('archived', 'shareHiddenFromTree', 'shareAlias', 'shareOmitDefaultCss'))
OR (type = 'relation' AND name IN ('imageLink', 'template', 'shareCss'))
)`, []);
AND noteId IN (${noteIdStr})`);
for (const row of rawAttributeRows) {
new Attribute(row);

View File

@@ -61,7 +61,7 @@ async function start() {
const parentNoteId = getRandomNoteId();
const prefix = Math.random() > 0.8 ? "prefix" : null;
const result = await cloningService.cloneNoteToParent(noteIdToClone, parentNoteId, prefix);
const result = await cloningService.cloneNoteToBranch(noteIdToClone, parentNoteId, prefix);
console.log(`Cloning ${i}:`, result.success ? "succeeded" : "FAILED");
}

View File

@@ -41,6 +41,8 @@
<%- include('dialogs/delete_notes.ejs') %>
<script type="text/javascript">
global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.baseApiUrl = 'api/';
window.device = "desktop";
window.glob = {

View File

@@ -105,6 +105,8 @@
<%- include('dialogs/confirm.ejs') %>
<script type="text/javascript">
global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.baseApiUrl = 'api/';
window.device = "mobile";
window.glob = {

View File

@@ -189,6 +189,8 @@
</div>
<script type="text/javascript">
global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.glob = {
sourceId: ''
};

View File

@@ -2,45 +2,79 @@
<html lang="en">
<head>
<meta charset="utf-8">
<% if (note.hasRelation("shareFavicon")) { %>
<link rel="shortcut icon" href="api/notes/<%= note.getRelation("shareFavicon").value %>/download">
<% } else { %>
<link rel="shortcut icon" href="../favicon.ico">
<% } %>
<script src="../app/share.js"></script>
<% if (!note.hasLabel("shareOmitDefaultCss")) { %>
<link href="../libraries/normalize.min.css" rel="stylesheet">
<link href="../stylesheets/share.css" rel="stylesheet">
<link href="../libraries/normalize.min.css" rel="stylesheet">
<link href="../stylesheets/share.css" rel="stylesheet">
<% } %>
<% if (note.type === 'text' || note.type === 'book') { %>
<link href="../libraries/ckeditor/ckeditor-content.css" rel="stylesheet">
<link href="../libraries/ckeditor/ckeditor-content.css" rel="stylesheet">
<% } %>
<% for (const cssRelation of note.getRelations("shareCss")) { %>
<link href="api/notes/<%= cssRelation.value %>/download" rel="stylesheet">
<link href="api/notes/<%= cssRelation.value %>/download" rel="stylesheet">
<% } %>
<% for (const jsRelation of note.getRelations("shareJs")) { %>
<script type="module" src="api/notes/<%= jsRelation.value %>/download"></script>
<% } %>
<%- header %>
<title><%= note.title %></title>
</head>
<body>
<div id="layout">
<div id="main">
<h1 id="title"><%= note.title %></h1>
<body data-note-id="<%= note.noteId %>">
<div id="layout">
<div id="main">
<% if (note.parents[0].noteId !== 'share' && note.parents.length !== 0) { %>
<nav id="parentLink">
parent: <a href="<%= note.parents[0].noteId %>"
class="type-<%= note.parents[0].type %>"><%= note.parents[0].title %></a>
</nav>
<% } %>
<div id="content" class="note-<%= note.type %> <% if (note.type === 'text') { %>ck-content<% } %>">
<h1 id="title"><%= note.title %></h1>
<% if (note.hasLabel("pageUrl")) { %>
<div id="noteClippedFrom">This note was originally clipped from <a href="<%= note.getLabelValue("pageUrl") %>"><%= note.getLabelValue("pageUrl") %></a></div>
<% } %>
<% if (note.type === 'book') { %>
<% } else if (isEmpty) { %>
<p>This note has no content.</p>
<% } else { %>
<div id="content" class="type-<%= note.type %><% if (note.type === 'text') { %> ck-content<% } %>">
<%- content %>
</div>
</div>
<% } %>
<% if (subRoot.hasChildren()) { %>
<button id="menuButton"></button>
<% if (note.hasChildren()) { %>
<nav id="childLinks" class="<% if (isEmpty) { %>grid<% } else { %>list<% } %>">
<% if (!isEmpty) { %>
<hr>
<span>Child notes: </span>
<% } %>
<ul>
<% for (const childNote of note.getChildNotes()) { %>
<li>
<a href="<%= childNote.shareId %>"
class="type-<%= childNote.type %>"><%= childNote.title %></a>
</li>
<% } %>
</ul>
</nav>
<% } %>
</div>
<% if (subRoot.hasChildren()) { %>
<button id="toggleMenuButton"></button>
<nav id="menu">
<%- include('tree_item', {note: subRoot, activeNote: note}) %>
</nav>
<% } %>
</div>
<script>
(function () {
const menuButton = document.getElementById('menuButton');
const layout = document.getElementById('layout');
menuButton.addEventListener('click', () => layout.classList.toggle('navMenu'));
}());
</script>
<% } %>
</div>
</body>
</html>

View File

@@ -2,7 +2,7 @@
<% if (activeNote.noteId === note.noteId) { %>
<strong><%= note.title %></strong>
<% } else { %>
<a href="./<%= note.shareId %>"><%= note.title %></a>
<a class="type-<%= note.type %>" href="./<%= note.shareId %>"><%= note.title %></a>
<% } %>
</p>

View File

@@ -11,5 +11,5 @@ module.exports = {
filename: 'desktop.js'
},
devtool: 'source-map',
target: 'electron-main'
target: 'electron-renderer'
};

View File

@@ -11,5 +11,5 @@ module.exports = {
filename: 'mobile.js'
},
devtool: 'source-map',
target: 'electron-main'
target: 'electron-renderer'
};

View File

@@ -11,5 +11,5 @@ module.exports = {
filename: 'setup.js'
},
devtool: 'source-map',
target: 'electron-main'
target: 'electron-renderer'
};