Merge remote-tracking branch 'origin/master' into m43

This commit is contained in:
zadam 2020-05-12 21:15:54 +02:00
commit 29e6b63f82
32 changed files with 205 additions and 124 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
libraries/jquery.min.map Normal file

File diff suppressed because one or more lines are too long

6
package-lock.json generated
View File

@ -3345,9 +3345,9 @@
} }
}, },
"electron": { "electron": {
"version": "9.0.0-beta.22", "version": "9.0.0-beta.24",
"resolved": "https://registry.npmjs.org/electron/-/electron-9.0.0-beta.22.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-9.0.0-beta.24.tgz",
"integrity": "sha512-dfqAf+CXXTKcNDj7DU7mYsmx+oZQcXOvJnZ8ZsgAHjrE9Tv8zsYUgCP3JlO4Z8CIazgleKXYmgh6H2stdK7fEA==", "integrity": "sha512-25L3XMqm/1CCaV5CgU5ZkhKXw9830WeipJrTW0+VC5XTKp/3xHwhxyQ5G1kQnOTJd7IGwOamvw237D6e1YKnng==",
"dev": true, "dev": true,
"requires": { "requires": {
"@electron/get": "^1.0.1", "@electron/get": "^1.0.1",

View File

@ -2,7 +2,7 @@
"name": "trilium", "name": "trilium",
"productName": "Trilium Notes", "productName": "Trilium Notes",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.42.1", "version": "0.42.2",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {
@ -78,7 +78,7 @@
"yazl": "^2.5.1" "yazl": "^2.5.1"
}, },
"devDependencies": { "devDependencies": {
"electron": "9.0.0-beta.22", "electron": "9.0.0-beta.24",
"electron-builder": "22.6.0", "electron-builder": "22.6.0",
"electron-packager": "14.2.1", "electron-packager": "14.2.1",
"electron-rebuild": "1.10.1", "electron-rebuild": "1.10.1",

View File

@ -105,7 +105,6 @@ class Attribute extends Entity {
// cannot be static! // cannot be static!
updatePojo(pojo) { updatePojo(pojo) {
delete pojo.isOwned;
delete pojo.__note; delete pojo.__note;
} }

View File

@ -411,10 +411,6 @@ class Note extends Entity {
} }
}); });
for (const attr of filteredAttributes) {
attr.isOwned = attr.noteId === this.noteId;
}
this.__attributeCache = filteredAttributes; this.__attributeCache = filteredAttributes;
} }

View File

@ -8,6 +8,7 @@ import contextMenu from "./services/context_menu.js";
import DesktopMainWindowLayout from "./layouts/desktop_main_window_layout.js"; import DesktopMainWindowLayout from "./layouts/desktop_main_window_layout.js";
import glob from "./services/glob.js"; import glob from "./services/glob.js";
import DesktopExtraWindowLayout from "./layouts/desktop_extra_window_layout.js"; import DesktopExtraWindowLayout from "./layouts/desktop_extra_window_layout.js";
import zoomService from './services/zoom.js';
glob.setupGlobs(); glob.setupGlobs();
@ -133,9 +134,11 @@ if (utils.isElectron()) {
return; return;
} }
const zoomLevel = zoomService.getCurrentZoom();
contextMenu.show({ contextMenu.show({
x: params.x, x: params.x / zoomLevel,
y: params.y, y: params.y / zoomLevel,
items, items,
selectMenuItemHandler: ({command, spellingSuggestion}) => { selectMenuItemHandler: ({command, spellingSuggestion}) => {
if (command === 'replaceMisspelling') { if (command === 'replaceMisspelling') {

View File

@ -59,8 +59,8 @@ function AttributesModel() {
}); });
}; };
async function showAttributes(attributes) { async function showAttributes(noteId, attributes) {
const ownedAttributes = attributes.filter(attr => attr.isOwned); const ownedAttributes = attributes.filter(attr => attr.noteId === noteId);
for (const attr of ownedAttributes) { for (const attr of ownedAttributes) {
attr.labelValue = attr.type === 'label' ? attr.value : ''; attr.labelValue = attr.type === 'label' ? attr.value : '';
@ -86,7 +86,7 @@ function AttributesModel() {
addLastEmptyRow(); addLastEmptyRow();
const inheritedAttributes = attributes.filter(attr => !attr.isOwned); const inheritedAttributes = attributes.filter(attr => attr.noteId !== noteId);
self.inheritedAttributes(inheritedAttributes); self.inheritedAttributes(inheritedAttributes);
} }
@ -96,7 +96,7 @@ function AttributesModel() {
const attributes = await server.get('notes/' + noteId + '/attributes'); const attributes = await server.get('notes/' + noteId + '/attributes');
await showAttributes(attributes); await showAttributes(noteId, attributes);
// attribute might not be rendered immediatelly so could not focus // attribute might not be rendered immediatelly so could not focus
setTimeout(() => $(".attribute-type-select:last").trigger('focus'), 1000); setTimeout(() => $(".attribute-type-select:last").trigger('focus'), 1000);
@ -166,7 +166,7 @@ function AttributesModel() {
const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave); const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave);
await showAttributes(attributes); await showAttributes(noteId, attributes);
toastService.showMessage("Attributes have been saved."); toastService.showMessage("Attributes have been saved.");
}; };

View File

@ -24,7 +24,6 @@ import NoteRevisionsWidget from "../widgets/collapsible_widgets/note_revisions.j
import SimilarNotesWidget from "../widgets/collapsible_widgets/similar_notes.js"; import SimilarNotesWidget from "../widgets/collapsible_widgets/similar_notes.js";
import WhatLinksHereWidget from "../widgets/collapsible_widgets/what_links_here.js"; import WhatLinksHereWidget from "../widgets/collapsible_widgets/what_links_here.js";
import SidePaneToggles from "../widgets/side_pane_toggles.js"; import SidePaneToggles from "../widgets/side_pane_toggles.js";
import appContext from "../services/app_context.js";
const RIGHT_PANE_CSS = ` const RIGHT_PANE_CSS = `
<style> <style>
@ -117,6 +116,7 @@ export default class DesktopMainWindowLayout {
.hideInZenMode()) .hideInZenMode())
.child(new FlexContainer('row') .child(new FlexContainer('row')
.collapsible() .collapsible()
.filling()
.child(new SidePaneContainer('left') .child(new SidePaneContainer('left')
.hideInZenMode() .hideInZenMode()
.child(new GlobalButtonsWidget()) .child(new GlobalButtonsWidget())

View File

@ -4,7 +4,7 @@ import DialogCommandExecutor from "./dialog_command_executor.js";
import Entrypoints from "./entrypoints.js"; import Entrypoints from "./entrypoints.js";
import options from "./options.js"; import options from "./options.js";
import utils from "./utils.js"; import utils from "./utils.js";
import ZoomService from "./zoom.js"; import zoomService from "./zoom.js";
import TabManager from "./tab_manager.js"; import TabManager from "./tab_manager.js";
import treeService from "./tree.js"; import treeService from "./tree.js";
import Component from "../widgets/component.js"; import Component from "../widgets/component.js";
@ -73,7 +73,7 @@ class AppContext extends Component {
} }
if (utils.isElectron()) { if (utils.isElectron()) {
this.child(new ZoomService()); this.child(zoomService);
} }
this.triggerEvent('initialRenderComplete'); this.triggerEvent('initialRenderComplete');

View File

@ -81,24 +81,29 @@ function goToLink(e) {
} }
else if (e.which === 1) { else if (e.which === 1) {
const activeTabContext = appContext.tabManager.getActiveTabContext(); const activeTabContext = appContext.tabManager.getActiveTabContext();
activeTabContext.setNote(notePath) activeTabContext.setNote(notePath);
} }
else { else {
return false; return false;
} }
} }
else { else {
const address = $link.attr('href'); if (e.which === 1) {
const address = $link.attr('href');
if (address && address.startsWith('http')) { if (address && address.startsWith('http')) {
window.open(address, '_blank'); window.open(address, '_blank');
}
}
else {
return false;
} }
} }
return true; return true;
} }
function newTabContextMenu(e) { function linkContextMenu(e) {
const $link = $(e.target).closest("a"); const $link = $(e.target).closest("a");
const notePath = getNotePathFromLink($link); const notePath = getNotePathFromLink($link);
@ -113,7 +118,7 @@ function newTabContextMenu(e) {
x: e.pageX, x: e.pageX,
y: e.pageY, y: e.pageY,
items: [ items: [
{title: "Open note in new tab", command: "openNoteInNewTab", uiIcon: "arrow-up-right"}, {title: "Open note in new tab", command: "openNoteInNewTab", uiIcon: "empty"},
{title: "Open note in new window", command: "openNoteInNewWindow", uiIcon: "window-open"} {title: "Open note in new window", command: "openNoteInNewWindow", uiIcon: "window-open"}
], ],
selectMenuItemHandler: ({command}) => { selectMenuItemHandler: ({command}) => {
@ -155,18 +160,20 @@ $(document).on('mousedown', '.note-detail-text a', function (e) {
$(document).on('mousedown', '.note-detail-book a', goToLink); $(document).on('mousedown', '.note-detail-book a', goToLink);
$(document).on('mousedown', '.note-detail-render a', goToLink); $(document).on('mousedown', '.note-detail-render a', goToLink);
$(document).on('mousedown', '.note-detail-text.ck-read-only a,.note-detail-text a.reference-link', goToLink); $(document).on('mousedown', '.note-detail-text a.reference-link', goToLink);
$(document).on('mousedown', '.note-detail-readonly-text a', goToLink);
$(document).on('mousedown', 'a.ck-link-actions__preview', goToLink); $(document).on('mousedown', 'a.ck-link-actions__preview', goToLink);
$(document).on('click', 'a.ck-link-actions__preview', e => { $(document).on('click', 'a.ck-link-actions__preview', e => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
}); });
$(document).on('contextmenu', 'a.ck-link-actions__preview', newTabContextMenu); $(document).on('contextmenu', 'a.ck-link-actions__preview', linkContextMenu);
$(document).on('contextmenu', '.note-detail-text a', newTabContextMenu); $(document).on('contextmenu', '.note-detail-text a', linkContextMenu);
$(document).on('contextmenu', "a[data-action='note']", newTabContextMenu); $(document).on('contextmenu', '.note-detail-readonly-text a', linkContextMenu);
$(document).on('contextmenu', ".note-detail-render a", newTabContextMenu); $(document).on('contextmenu', "a[data-action='note']", linkContextMenu);
$(document).on('contextmenu', ".note-paths-widget a", newTabContextMenu); $(document).on('contextmenu', ".note-detail-render a", linkContextMenu);
$(document).on('contextmenu', ".note-paths-widget a", linkContextMenu);
export default { export default {
getNotePathFromUrl, getNotePathFromUrl,

View File

@ -37,6 +37,10 @@ function subscribeToMessages(messageHandler) {
// used to serialize sync operations // used to serialize sync operations
let consumeQueuePromise = null; let consumeQueuePromise = null;
// most sync events are sent twice - once immediatelly after finishing the transaction and once during the scheduled ping
// but we want to process only once
const receivedSyncIds = new Set();
async function handleMessage(event) { async function handleMessage(event) {
const message = JSON.parse(event.data); const message = JSON.parse(event.data);
@ -52,14 +56,19 @@ async function handleMessage(event) {
if (syncRows.length > 0) { if (syncRows.length > 0) {
const filteredRows = syncRows.filter(row => const filteredRows = syncRows.filter(row =>
row.entityName !== 'recent_notes' !receivedSyncIds.has(row.id)
&& row.entityName !== 'recent_notes'
&& (row.entityName !== 'options' || row.entityId !== 'openTabs')); && (row.entityName !== 'options' || row.entityId !== 'openTabs'));
if (filteredRows.length > 0) { if (filteredRows.length > 0) {
console.debug(utils.now(), "Sync data: ", filteredRows); console.debug(utils.now(), "Sync data: ", filteredRows);
} }
syncDataQueue.push(...syncRows); for (const row of filteredRows) {
receivedSyncIds.add(row.id);
}
syncDataQueue.push(...filteredRows);
// we set lastAcceptedSyncId even before sync processing and send ping so that backend can start sending more updates // we set lastAcceptedSyncId even before sync processing and send ping so that backend can start sending more updates
lastAcceptedSyncId = Math.max(lastAcceptedSyncId, syncRows[syncRows.length - 1].id); lastAcceptedSyncId = Math.max(lastAcceptedSyncId, syncRows[syncRows.length - 1].id);
@ -170,7 +179,7 @@ function connectWebSocket() {
async function sendPing() { async function sendPing() {
if (Date.now() - lastPingTs > 30000) { if (Date.now() - lastPingTs > 30000) {
console.log(utils.now(), "Lost websocket connection to the backend"); console.log(utils.now(), "Lost websocket connection to the backend. If you keep having this issue repeatedly, you might want to check your reverse proxy (nginx, apache) configuration and allow/unblock WebSocket.");
} }
if (ws.readyState === ws.OPEN) { if (ws.readyState === ws.OPEN) {

View File

@ -5,11 +5,13 @@ import utils from "../services/utils.js";
const MIN_ZOOM = 0.5; const MIN_ZOOM = 0.5;
const MAX_ZOOM = 2.0; const MAX_ZOOM = 2.0;
export default class ZoomService extends Component { class ZoomService extends Component {
constructor() { constructor() {
super(); super();
this.setZoomFactor(options.getFloat('zoomFactor')); options.initializedPromise.then(() => {
this.setZoomFactor(options.getFloat('zoomFactor'));
});
} }
setZoomFactor(zoomFactor) { setZoomFactor(zoomFactor) {
@ -46,3 +48,7 @@ export default class ZoomService extends Component {
this.setZoomFactorAndSave(zoomFactor); this.setZoomFactorAndSave(zoomFactor);
} }
} }
const zoomService = new ZoomService();
export default zoomService;

View File

@ -30,6 +30,11 @@ class BasicWidget extends Component {
return this; return this;
} }
filling() {
this.css('flex-grow', '1');
return this;
}
hideInZenMode() { hideInZenMode() {
this.class('hide-in-zen-mode'); this.class('hide-in-zen-mode');
return this; return this;

View File

@ -251,8 +251,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
this.triggerCommand('setActiveScreen', {screen:'detail'}); this.triggerCommand('setActiveScreen', {screen:'detail'});
} }
}, },
expand: (event, data) => this.setExpandedToServer(data.node.data.branchId, true), expand: (event, data) => this.setExpanded(data.node.data.branchId, true),
collapse: (event, data) => this.setExpandedToServer(data.node.data.branchId, false), collapse: (event, data) => this.setExpanded(data.node.data.branchId, false),
hotkeys: utils.isMobile() ? undefined : { keydown: await this.getHotKeys() }, hotkeys: utils.isMobile() ? undefined : { keydown: await this.getHotKeys() },
dnd5: { dnd5: {
autoExpandMS: 600, autoExpandMS: 600,
@ -807,7 +807,9 @@ export default class NoteTreeWidget extends TabAwareWidget {
async entitiesReloadedEvent({loadResults}) { async entitiesReloadedEvent({loadResults}) {
const activeNode = this.getActiveNode(); const activeNode = this.getActiveNode();
const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null; const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null;
const activeNoteId = activeNode ? activeNode.data.noteId : null; const activeNoteId = activeNode ? activeNode.data.noteId : null;
const noteIdsToUpdate = new Set(); const noteIdsToUpdate = new Set();
@ -929,15 +931,27 @@ export default class NoteTreeWidget extends TabAwareWidget {
if (node) { if (node) {
node.setActive(true, {noEvents: true}); node.setActive(true, {noEvents: true});
} }
else {
// this is used when original note has been deleted and we want to move the focus to the note above/below
node = await this.expandToNote(nextNotePath);
if (node) {
this.tree.setFocus();
node.setFocus(true);
await appContext.tabManager.getActiveTabContext().setNote(nextNotePath);
}
}
} }
} }
async setExpandedToServer(branchId, isExpanded) { async setExpanded(branchId, isExpanded) {
utils.assertArguments(branchId); utils.assertArguments(branchId);
const expandedNum = isExpanded ? 1 : 0; const branch = treeCache.getBranch(branchId);
branch.isExpanded = isExpanded;
await server.put('branches/' + branchId + '/expanded/' + expandedNum); await server.put(`branches/${branchId}/expanded/${isExpanded ? 1 : 0}`);
} }
async reloadTreeFromCache() { async reloadTreeFromCache() {

View File

@ -19,6 +19,7 @@ const TPL = `
.promoted-attributes td, .promoted-attributes th { .promoted-attributes td, .promoted-attributes th {
padding: 5px; padding: 5px;
min-width: 50px; /* otherwise checkboxes can collapse into 0 width (if there are only checkboxes) */
} }
</style> </style>
@ -98,7 +99,7 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
const $labelCell = $("<th>").append(valueAttr.name); const $labelCell = $("<th>").append(valueAttr.name);
const $input = $("<input>") const $input = $("<input>")
.prop("tabindex", definitionAttr.position) .prop("tabindex", definitionAttr.position)
.prop("attribute-id", valueAttr.isOwned ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one .prop("attribute-id", valueAttr.noteId === this.noteId ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one
.prop("attribute-type", valueAttr.type) .prop("attribute-type", valueAttr.type)
.prop("attribute-name", valueAttr.name) .prop("attribute-name", valueAttr.name)
.prop("value", valueAttr.value) .prop("value", valueAttr.value)

View File

@ -602,18 +602,23 @@ export default class TabRowWidget extends BasicWidget {
} }
updateTab($tab, note) { updateTab($tab, note) {
if (!note || !$tab.length) { if (!$tab.length) {
return; return;
} }
this.updateTitle($tab, note.title);
for (const clazz of Array.from($tab[0].classList)) { // create copy to safely iterate over while removing classes for (const clazz of Array.from($tab[0].classList)) { // create copy to safely iterate over while removing classes
if (clazz !== 'note-tab') { if (clazz !== 'note-tab') {
$tab.removeClass(clazz); $tab.removeClass(clazz);
} }
} }
if (!note) {
this.updateTitle($tab, 'New tab');
return;
}
this.updateTitle($tab, note.title);
$tab.addClass(note.getCssClass()); $tab.addClass(note.getCssClass());
$tab.addClass(utils.getNoteTypeClass(note.type)); $tab.addClass(utils.getNoteTypeClass(note.type));
$tab.addClass(utils.getMimeTypeClass(note.mime)); $tab.addClass(utils.getMimeTypeClass(note.mime));

View File

@ -22,6 +22,10 @@ const TPL = `
.note-detail-readonly-text p:first-child, .note-detail-text::before { .note-detail-readonly-text p:first-child, .note-detail-text::before {
margin-top: 0; margin-top: 0;
} }
.note-detail-readonly-text img {
max-width: 100%;
}
</style> </style>
<div class="alert alert-warning no-print"> <div class="alert alert-warning no-print">

View File

@ -1,6 +1,5 @@
"use strict"; "use strict";
const noteService = require('../../services/notes');
const protectedSessionService = require('../../services/protected_session'); const protectedSessionService = require('../../services/protected_session');
const repository = require('../../services/repository'); const repository = require('../../services/repository');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
@ -45,7 +44,9 @@ async function downloadNoteFile(noteId, res, contentDisposition = true) {
if (contentDisposition) { if (contentDisposition) {
// (one) reason we're not using the originFileName (available as label) is that it's not // (one) reason we're not using the originFileName (available as label) is that it's not
// available for older note revisions and thus would be inconsistent // available for older note revisions and thus would be inconsistent
res.setHeader('Content-Disposition', utils.getContentDisposition(note.title || "untitled")); const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
} }
res.setHeader('Content-Type', note.mime); res.setHeader('Content-Type', note.mime);

View File

@ -38,13 +38,7 @@ async function getNoteRevision(req) {
* @return {string} * @return {string}
*/ */
function getRevisionFilename(noteRevision) { function getRevisionFilename(noteRevision) {
let filename = noteRevision.title || "untitled"; let filename = utils.formatDownloadTitle(noteRevision.title, noteRevision.type, noteRevision.mime);
if (noteRevision.type === 'text') {
filename += '.html';
} else if (['relation-map', 'search'].includes(noteRevision.type)) {
filename += '.json';
}
const extension = path.extname(filename); const extension = path.extname(filename);
const date = noteRevision.dateCreated const date = noteRevision.dateCreated

View File

@ -1 +1 @@
module.exports = { buildDate:"2020-05-06T23:24:13+02:00", buildRevision: "54ecd2ee75d1177cedadf9fee10319687feee5f0" }; module.exports = { buildDate:"2020-05-12T16:46:45+02:00", buildRevision: "4f50864ec8346a12d7845cb4c91a3de3b1043d34" };

View File

@ -13,6 +13,7 @@ const Attribute = require('../entities/attribute');
const hoistedNoteService = require('../services/hoisted_note'); const hoistedNoteService = require('../services/hoisted_note');
const protectedSessionService = require('../services/protected_session'); const protectedSessionService = require('../services/protected_session');
const log = require('../services/log'); const log = require('../services/log');
const utils = require('../services/utils');
const noteRevisionService = require('../services/note_revisions'); const noteRevisionService = require('../services/note_revisions');
const attributeService = require('../services/attributes'); const attributeService = require('../services/attributes');
const request = require('./request'); const request = require('./request');
@ -276,9 +277,9 @@ async function downloadImage(noteId, imageUrl) {
const downloadImagePromises = {}; const downloadImagePromises = {};
function replaceUrl(content, url, imageNote) { function replaceUrl(content, url, imageNote) {
const quoted = url.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); const quotedUrl = utils.quoteRegex(url);
return content.replace(new RegExp(`\\s+src=[\"']${quoted}[\"']`, "g"), ` src="api/images/${imageNote.noteId}/${imageNote.title}"`); return content.replace(new RegExp(`\\s+src=[\"']${quotedUrl}[\"']`, "g"), ` src="api/images/${imageNote.noteId}/${imageNote.title}"`);
} }
async function downloadImages(noteId, content) { async function downloadImages(noteId, content) {

View File

@ -1,6 +1,6 @@
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const filterRegex = /(\b(AND|OR)\s+)?@(!?)([\p{L}\p{Number}_]+|"[^"]+")\s*((=|!=|<|<=|>|>=|!?\*=|!?=\*|!?\*=\*)\s*([^\s=*]+|"[^"]+"))?/igu; const filterRegex = /(\b(AND|OR)\s+)?@(!?)([\p{L}\p{Number}_]+|"[^"]+")\s*((=|!=|<|<=|>|>=|!?\*=|!?=\*|!?\*=\*)\s*([^\s=*"]+|"[^"]+"))?/igu;
const smartValueRegex = /^(NOW|TODAY|WEEK|MONTH|YEAR) *([+\-] *\d+)?$/i; const smartValueRegex = /^(NOW|TODAY|WEEK|MONTH|YEAR) *([+\-] *\d+)?$/i;
function calculateSmartValue(v) { function calculateSmartValue(v) {

View File

@ -221,6 +221,7 @@ async function transactional(func) {
await commit(); await commit();
// note that sync rows sent from this action will be sent again by scheduled periodic ping
require('./ws.js').sendPingToAllClients(); require('./ws.js').sendPingToAllClients();
transactionActive = false; transactionActive = false;

View File

@ -5,6 +5,7 @@ const randtoken = require('rand-token').generator({source: 'crypto'});
const unescape = require('unescape'); const unescape = require('unescape');
const escape = require('escape-html'); const escape = require('escape-html');
const sanitize = require("sanitize-filename"); const sanitize = require("sanitize-filename");
const mimeTypes = require('mime-types');
function newEntityId() { function newEntityId() {
return randomString(12); return randomString(12);
@ -166,10 +167,46 @@ function isStringNote(type, mime) {
|| STRING_MIME_TYPES.includes(mime); || STRING_MIME_TYPES.includes(mime);
} }
function replaceAll(string, replaceWhat, replaceWith) { function quoteRegex(url) {
const escapedWhat = replaceWhat.replace(/([\/,!\\^${}\[\]().*+?|<>\-&])/g, "\\$&"); return url.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
}
return string.replace(new RegExp(escapedWhat, "g"), replaceWith); function replaceAll(string, replaceWhat, replaceWith) {
const quotedReplaceWhat = quoteRegex(replaceWhat);
return string.replace(new RegExp(quotedReplaceWhat, "g"), replaceWith);
}
function formatDownloadTitle(filename, type, mime) {
if (!filename) {
filename = "untitled";
}
if (type === 'text') {
return filename + '.html';
} else if (['relation-map', 'search'].includes(type)) {
return filename + '.json';
} else {
if (!mime) {
return filename;
}
mime = mime.toLowerCase();
const filenameLc = filename.toLowerCase();
const extensions = mimeTypes.extensions[mime];
if (!extensions || extensions.length === 0) {
return filename;
}
for (const ext of extensions) {
if (filenameLc.endsWith('.' + ext)) {
return filename;
}
}
return filename + '.' + extensions[0];
}
} }
module.exports = { module.exports = {
@ -198,5 +235,7 @@ module.exports = {
sanitizeFilenameForHeader, sanitizeFilenameForHeader,
getContentDisposition, getContentDisposition,
isStringNote, isStringNote,
replaceAll quoteRegex,
replaceAll,
formatDownloadTitle
}; };

View File

@ -5,7 +5,7 @@
<link rel="shortcut icon" href="favicon.ico"> <link rel="shortcut icon" href="favicon.ico">
<title>Trilium Notes</title> <title>Trilium Notes</title>
</head> </head>
<body class="desktop theme-<%= theme %>" style="display: none; --main-font-size: <%= mainFontSize %>%; --tree-font-size: <%= treeFontSize %>%; --detail-font-size: <%= detailFontSize %>%;"> <body class="desktop theme-<%= theme %>" style="--main-font-size: <%= mainFontSize %>%; --tree-font-size: <%= treeFontSize %>%; --detail-font-size: <%= detailFontSize %>%;">
<noscript>Trilium requires JavaScript to be enabled.</noscript> <noscript>Trilium requires JavaScript to be enabled.</noscript>
<div id="toast-container" class="d-flex flex-column justify-content-center align-items-center"></div> <div id="toast-container" class="d-flex flex-column justify-content-center align-items-center"></div>
@ -82,9 +82,5 @@
<link rel="stylesheet" type="text/css" href="libraries/boxicons/css/boxicons.min.css"> <link rel="stylesheet" type="text/css" href="libraries/boxicons/css/boxicons.min.css">
<script>
$("body").show();
</script>
</body> </body>
</html> </html>