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

# Conflicts:
#	package-lock.json
#	src/public/app/services/note_content_renderer.js
#	src/public/stylesheets/style.css
#	src/routes/api/files.js
#	src/routes/routes.js
This commit is contained in:
zadam 2021-04-25 11:14:45 +02:00
commit 7494491560
55 changed files with 378 additions and 165 deletions

3
.idea/vcs.xml generated
View File

@ -2,6 +2,5 @@
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" /> <mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component> </component>
</project> </project>

View File

@ -10,7 +10,7 @@ fi
cd dist cd dist
wget https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz wget https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz
tar xvfJ node-v${NODE_VERSION}-linux-x64.tar.xz tar xfJ node-v${NODE_VERSION}-linux-x64.tar.xz
rm node-v${NODE_VERSION}-linux-x64.tar.xz rm node-v${NODE_VERSION}-linux-x64.tar.xz
cd .. cd ..

View File

@ -5,7 +5,7 @@ if [[ $# -eq 0 ]] ; then
exit 1 exit 1
fi fi
npm run webpack n exec 12 npm run webpack
DIR=$1 DIR=$1
@ -27,7 +27,7 @@ cp -r electron.js $DIR/
cp webpack-* $DIR/ cp webpack-* $DIR/
# run in subshell (so we return to original dir) # run in subshell (so we return to original dir)
(cd $DIR && npm install --only=prod) (cd $DIR && n exec 12 npm install --only=prod)
# cleanup of useless files in dependencies # cleanup of useless files in dependencies
rm -r $DIR/node_modules/image-q/demo rm -r $DIR/node_modules/image-q/demo

View File

@ -8,9 +8,9 @@ fi
VERSION=$1 VERSION=$1
SERIES=${VERSION:0:4}-latest SERIES=${VERSION:0:4}-latest
sudo docker push zadam/trilium:$VERSION docker push zadam/trilium:$VERSION
sudo docker push zadam/trilium:$SERIES docker push zadam/trilium:$SERIES
if [[ $1 != *"beta"* ]]; then if [[ $1 != *"beta"* ]]; then
sudo docker push zadam/trilium:latest docker push zadam/trilium:latest
fi fi

View File

@ -55,47 +55,20 @@ echo "Creating release in GitHub"
EXTRA= EXTRA=
if [[ $TAG == *"beta"* ]]; then if [[ $TAG == *"beta"* ]]; then
EXTRA=--pre-release EXTRA=--prerelease
fi fi
github-release release \ echo "$GITHUB_CLI_AUTH_TOKEN" | gh auth login --with-token
--tag $TAG \
--name "$TAG release" $EXTRA
echo "Uploading debian x64 package" gh release create "$TAG" \
--title "$TAG release" \
github-release upload \ --notes "" \
--tag $TAG \ $EXTRA \
--name "$DEBIAN_X64_BUILD" \ "dist/$DEBIAN_X64_BUILD" \
--file "dist/$DEBIAN_X64_BUILD" "dist/$LINUX_X64_BUILD" \
"dist/$WINDOWS_X64_BUILD" \
echo "Uploading linux x64 build" "dist/$MAC_X64_BUILD" \
"dist/$SERVER_BUILD"
github-release upload \
--tag $TAG \
--name "$LINUX_X64_BUILD" \
--file "dist/$LINUX_X64_BUILD"
echo "Uploading windows x64 build"
github-release upload \
--tag $TAG \
--name "$WINDOWS_X64_BUILD" \
--file "dist/$WINDOWS_X64_BUILD"
echo "Uploading mac x64 build"
github-release upload \
--tag $TAG \
--name "$MAC_X64_BUILD" \
--file "dist/$MAC_X64_BUILD"
echo "Uploading linux x64 server build"
github-release upload \
--tag $TAG \
--name "$SERVER_BUILD" \
--file "dist/$SERVER_BUILD"
echo "Building docker image" echo "Building docker image"
@ -105,4 +78,4 @@ echo "Pushing docker image to dockerhub"
bin/push-docker-image.sh $VERSION bin/push-docker-image.sh $VERSION
echo "Release finished!" echo "Release finished!"

View File

@ -22,7 +22,7 @@ app.on('window-all-closed', () => {
}); });
app.on('ready', async () => { app.on('ready', async () => {
app.setAppUserModelId('com.github.zadam.trilium'); // app.setAppUserModelId('com.github.zadam.trilium');
// if db is not initialized -> setup process // if db is not initialized -> setup process
// if db is initialized, then we need to wait until the migration process is finished // if db is initialized, then we need to wait until the migration process is finished

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@
"name": "trilium", "name": "trilium",
"productName": "Trilium Notes", "productName": "Trilium Notes",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.47.0-beta", "version": "0.47.1-beta",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {
@ -28,6 +28,7 @@
"axios": "0.21.1", "axios": "0.21.1",
"better-sqlite3": "7.1.4", "better-sqlite3": "7.1.4",
"body-parser": "1.19.0", "body-parser": "1.19.0",
"chokidar": "^3.5.1",
"cls-hooked": "4.2.2", "cls-hooked": "4.2.2",
"commonmark": "0.29.3", "commonmark": "0.29.3",
"cookie-parser": "1.4.5", "cookie-parser": "1.4.5",
@ -42,7 +43,7 @@
"express-partial-content": "^1.0.2", "express-partial-content": "^1.0.2",
"express-session": "1.17.1", "express-session": "1.17.1",
"fs-extra": "9.1.0", "fs-extra": "9.1.0",
"helmet": "4.4.1", "helmet": "4.5.0",
"html": "1.0.0", "html": "1.0.0",
"html2plaintext": "2.1.2", "html2plaintext": "2.1.2",
"http-proxy-agent": "4.0.1", "http-proxy-agent": "4.0.1",
@ -53,11 +54,11 @@
"is-svg": "4.3.1", "is-svg": "4.3.1",
"jimp": "0.16.1", "jimp": "0.16.1",
"joplin-turndown-plugin-gfm": "1.0.12", "joplin-turndown-plugin-gfm": "1.0.12",
"jsdom": "16.5.2", "jsdom": "16.5.3",
"mime-types": "2.1.30", "mime-types": "2.1.30",
"multer": "1.4.2", "multer": "1.4.2",
"node-abi": "2.21.0", "node-abi": "2.26.0",
"open": "8.0.5", "open": "8.0.6",
"portscanner": "2.2.0", "portscanner": "2.2.0",
"rand-token": "1.0.1", "rand-token": "1.0.1",
"request": "^2.88.2", "request": "^2.88.2",
@ -73,13 +74,13 @@
"tmp": "^0.2.1", "tmp": "^0.2.1",
"turndown": "7.0.0", "turndown": "7.0.0",
"unescape": "1.0.1", "unescape": "1.0.1",
"ws": "7.4.4", "ws": "7.4.5",
"yauzl": "2.10.0", "yauzl": "2.10.0",
"yazl": "2.5.1" "yazl": "2.5.1"
}, },
"devDependencies": { "devDependencies": {
"cross-env": "7.0.3", "cross-env": "7.0.3",
"electron": "13.0.0-beta.12", "electron": "13.0.0-beta.17",
"electron-builder": "22.10.5", "electron-builder": "22.10.5",
"electron-packager": "15.2.0", "electron-packager": "15.2.0",
"electron-rebuild": "2.3.5", "electron-rebuild": "2.3.5",
@ -88,7 +89,7 @@
"jsdoc": "3.6.6", "jsdoc": "3.6.6",
"lorem-ipsum": "2.0.3", "lorem-ipsum": "2.0.3",
"rcedit": "3.0.0", "rcedit": "3.0.0",
"webpack": "5.31.2", "webpack": "5.35.1",
"webpack-cli": "4.6.0" "webpack-cli": "4.6.0"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@ -121,14 +121,14 @@ ws.subscribeToMessages(async message => {
return; return;
} }
if (message.type === 'task-error') { if (message.type === 'taskError') {
toastService.closePersistent(message.taskId); toastService.closePersistent(message.taskId);
toastService.showError(message.message); toastService.showError(message.message);
} }
else if (message.type === 'task-progress-count') { else if (message.type === 'taskProgressCount') {
toastService.showPersistent(makeToast(message.taskId, "Export in progress: " + message.progressCount)); toastService.showPersistent(makeToast(message.taskId, "Export in progress: " + message.progressCount));
} }
else if (message.type === 'task-succeeded') { else if (message.type === 'taskSucceeded') {
const toast = makeToast(message.taskId, "Export finished successfully."); const toast = makeToast(message.taskId, "Export finished successfully.");
toast.closeAfter = 5000; toast.closeAfter = 5000;

View File

@ -36,6 +36,7 @@ import SearchResultWidget from "../widgets/search_result.js";
import SyncStatusWidget from "../widgets/sync_status.js"; import SyncStatusWidget from "../widgets/sync_status.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js"; import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import RootContainer from "../widgets/containers/root_container.js"; import RootContainer from "../widgets/containers/root_container.js";
import NoteUpdateStatusWidget from "../widgets/note_update_status.js";
const RIGHT_PANE_CSS = ` const RIGHT_PANE_CSS = `
<style> <style>
@ -177,6 +178,7 @@ export default class DesktopLayout {
.child(new InheritedAttributesWidget()) .child(new InheritedAttributesWidget())
) )
) )
.child(new NoteUpdateStatusWidget())
.child( .child(
new TabCachingWidget(() => new ScrollingContainer() new TabCachingWidget(() => new ScrollingContainer()
.child(new SqlTableSchemasWidget()) .child(new SqlTableSchemasWidget())

View File

@ -1,6 +1,6 @@
import froca from "./froca.js"; import froca from "./froca.js";
import bundleService from "./bundle.js"; import bundleService from "./bundle.js";
import DialogCommandExecutor from "./dialog_command_executor.js"; import RootCommandExecutor from "./root_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";
@ -57,7 +57,7 @@ class AppContext extends Component {
this.executors = [ this.executors = [
this.tabManager, this.tabManager,
new DialogCommandExecutor(), new RootCommandExecutor(),
new Entrypoints(), new Entrypoints(),
new MainTreeExecutors() new MainTreeExecutors()
]; ];

View File

@ -153,12 +153,12 @@ ws.subscribeToMessages(async message => {
return; return;
} }
if (message.type === 'task-error') { if (message.type === 'taskError') {
toastService.closePersistent(message.taskId); toastService.closePersistent(message.taskId);
toastService.showError(message.message); toastService.showError(message.message);
} else if (message.type === 'task-progress-count') { } else if (message.type === 'taskProgressCount') {
toastService.showPersistent(makeToast(message.taskId, "Delete notes in progress: " + message.progressCount)); toastService.showPersistent(makeToast(message.taskId, "Delete notes in progress: " + message.progressCount));
} else if (message.type === 'task-succeeded') { } else if (message.type === 'taskSucceeded') {
const toast = makeToast(message.taskId, "Delete finished successfully."); const toast = makeToast(message.taskId, "Delete finished successfully.");
toast.closeAfter = 5000; toast.closeAfter = 5000;
@ -167,16 +167,16 @@ ws.subscribeToMessages(async message => {
}); });
ws.subscribeToMessages(async message => { ws.subscribeToMessages(async message => {
if (message.taskType !== 'undelete-notes') { if (message.taskType !== 'undeleteNotes') {
return; return;
} }
if (message.type === 'task-error') { if (message.type === 'taskError') {
toastService.closePersistent(message.taskId); toastService.closePersistent(message.taskId);
toastService.showError(message.message); toastService.showError(message.message);
} else if (message.type === 'task-progress-count') { } else if (message.type === 'taskProgressCount') {
toastService.showPersistent(makeToast(message.taskId, "Undeleting notes in progress: " + message.progressCount)); toastService.showPersistent(makeToast(message.taskId, "Undeleting notes in progress: " + message.progressCount));
} else if (message.type === 'task-succeeded') { } else if (message.type === 'taskSucceeded') {
const toast = makeToast(message.taskId, "Undeleting notes finished successfully."); const toast = makeToast(message.taskId, "Undeleting notes finished successfully.");
toast.closeAfter = 5000; toast.closeAfter = 5000;

View File

@ -64,13 +64,12 @@ async function getWidgetBundlesByParent() {
try { try {
widget = await executeBundle(bundle); widget = await executeBundle(bundle);
widgetsByParent.add(widget);
} }
catch (e) { catch (e) {
logError("Widget initialization failed: ", e); logError("Widget initialization failed: ", e);
continue; continue;
} }
widgetsByParent.add(widget);
} }
return widgetsByParent; return widgetsByParent;

View File

@ -0,0 +1,36 @@
import ws from "./ws.js";
import appContext from "./app_context.js";
const fileModificationStatus = {};
function getFileModificationStatus(noteId) {
return fileModificationStatus[noteId];
}
function fileModificationUploaded(noteId) {
delete fileModificationStatus[noteId];
}
function ignoreModification(noteId) {
delete fileModificationStatus[noteId];
}
ws.subscribeToMessages(async message => {
if (message.type !== 'openedFileUpdated') {
return;
}
fileModificationStatus[message.noteId] = message;
appContext.triggerEvent('openedFileUpdated', {
noteId: message.noteId,
lastModifiedMs: message.lastModifiedMs,
filePath: message.filePath
});
});
export default {
getFileModificationStatus,
fileModificationUploaded,
ignoreModification
}

View File

@ -52,12 +52,12 @@ ws.subscribeToMessages(async message => {
return; return;
} }
if (message.type === 'task-error') { if (message.type === 'taskError') {
toastService.closePersistent(message.taskId); toastService.closePersistent(message.taskId);
toastService.showError(message.message); toastService.showError(message.message);
} else if (message.type === 'task-progress-count') { } else if (message.type === 'taskProgressCount') {
toastService.showPersistent(makeToast(message.taskId, "Import in progress: " + message.progressCount)); toastService.showPersistent(makeToast(message.taskId, "Import in progress: " + message.progressCount));
} else if (message.type === 'task-succeeded') { } else if (message.type === 'taskSucceeded') {
const toast = makeToast(message.taskId, "Import finished successfully."); const toast = makeToast(message.taskId, "Import finished successfully.");
toast.closeAfter = 5000; toast.closeAfter = 5000;

View File

@ -89,7 +89,11 @@ function updateDisplayedShortcuts($container) {
const action = await getAction(actionName, true); const action = await getAction(actionName, true);
if (action) { if (action) {
$(el).text(action.effectiveShortcuts.join(', ')); const keyboardActions = action.effectiveShortcuts.join(', ');
if (keyboardActions || $(el).text() !== "not set") {
$(el).text(keyboardActions);
}
} }
}); });

View File

@ -39,12 +39,12 @@ async function getRenderedContent(note, options = {}) {
.css("max-width", "100%") .css("max-width", "100%")
); );
} }
else if (!options.tooltip && ['file', 'pdf', 'audio', 'video']) { else if (!options.tooltip && ['file', 'pdf', 'audio', 'video'].includes(type)) {
const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>'); const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>');
const $openButton = $('<button class="file-open btn btn-primary" type="button">Open</button>'); const $openButton = $('<button class="file-open btn btn-primary" type="button">Open</button>');
$downloadButton.on('click', () => openService.downloadFileNote(note.noteId)); $downloadButton.on('click', () => openService.downloadFileNote(note.noteId));
$openButton.on('click', () => openService.openFileNote(note.noteId)); $openButton.on('click', () => openService.openNoteExternally(note.noteId));
// open doesn't work for protected notes since it works through browser which isn't in protected session // open doesn't work for protected notes since it works through browser which isn't in protected session
$openButton.toggle(!note.isProtected); $openButton.toggle(!note.isProtected);
@ -59,7 +59,7 @@ async function getRenderedContent(note, options = {}) {
} }
else if (type === 'audio') { else if (type === 'audio') {
const $audioPreview = $('<audio controls></audio>') const $audioPreview = $('<audio controls></audio>')
.attr("src", openService.getUrlForDownload("api/notes/" + note.noteId + "/open")) .attr("src", openService.getUrlForStreaming("api/notes/" + note.noteId + "/open-partial"))
.attr("type", note.mime) .attr("type", note.mime)
.css("width", "100%"); .css("width", "100%");
@ -67,7 +67,7 @@ async function getRenderedContent(note, options = {}) {
} }
else if (type === 'video') { else if (type === 'video') {
const $videoPreview = $('<video controls></video>') const $videoPreview = $('<video controls></video>')
.attr("src", openService.getUrlForDownload("api/notes/" + note.noteId + "/open")) .attr("src", openService.getUrlForDownload("api/notes/" + note.noteId + "/open-partial"))
.attr("type", note.mime) .attr("type", note.mime)
.css("width", "100%"); .css("width", "100%");

View File

@ -317,7 +317,9 @@ class NoteListRenderer {
const $expander = $('<span class="note-expander bx bx-chevron-right"></span>'); const $expander = $('<span class="note-expander bx bx-chevron-right"></span>');
const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note); const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note);
const notePath = this.parentNote.noteId + '/' + note.noteId; const notePath = this.parentNote.type === 'search'
? note.noteId // for search note parent we want to display non-search path
: this.parentNote.noteId + '/' + note.noteId;
const $card = $('<div class="note-book-card">') const $card = $('<div class="note-book-card">')
.attr('data-note-id', note.noteId) .attr('data-note-id', note.noteId)

View File

@ -21,9 +21,9 @@ function downloadFileNote(noteId) {
download(url); download(url);
} }
async function openFileNote(noteId) { async function openNoteExternally(noteId) {
if (utils.isElectron()) { if (utils.isElectron()) {
const resp = await server.post("notes/" + noteId + "/saveToTmpDir"); const resp = await server.post("notes/" + noteId + "/save-to-tmp-dir");
const electron = utils.dynamicRequire('electron'); const electron = utils.dynamicRequire('electron');
const res = await electron.shell.openPath(resp.tmpFilePath); const res = await electron.shell.openPath(resp.tmpFilePath);
@ -66,7 +66,7 @@ function getHost() {
export default { export default {
download, download,
downloadFileNote, downloadFileNote,
openFileNote, openNoteExternally,
downloadNoteRevision, downloadNoteRevision,
getUrlForDownload getUrlForDownload
} }

View File

@ -89,18 +89,18 @@ function makeToast(message, protectingLabel, text) {
} }
ws.subscribeToMessages(async message => { ws.subscribeToMessages(async message => {
if (message.taskType !== 'protect-notes') { if (message.taskType !== 'protectNotes') {
return; return;
} }
const protectingLabel = message.data.protect ? "Protecting" : "Unprotecting"; const protectingLabel = message.data.protect ? "Protecting" : "Unprotecting";
if (message.type === 'task-error') { if (message.type === 'taskError') {
toastService.closePersistent(message.taskId); toastService.closePersistent(message.taskId);
toastService.showError(message.message); toastService.showError(message.message);
} else if (message.type === 'task-progress-count') { } else if (message.type === 'taskProgressCount') {
toastService.showPersistent(makeToast(message, protectingLabel,protectingLabel + " in progress: " + message.progressCount)); toastService.showPersistent(makeToast(message, protectingLabel,protectingLabel + " in progress: " + message.progressCount));
} else if (message.type === 'task-succeeded') { } else if (message.type === 'taskSucceeded') {
const toast = makeToast(message, protectingLabel, protectingLabel + " finished successfully."); const toast = makeToast(message, protectingLabel, protectingLabel + " finished successfully.");
toast.closeAfter = 3000; toast.closeAfter = 3000;

View File

@ -2,8 +2,9 @@ import Component from "../widgets/component.js";
import appContext from "./app_context.js"; import appContext from "./app_context.js";
import dateNoteService from "../services/date_notes.js"; import dateNoteService from "../services/date_notes.js";
import treeService from "../services/tree.js"; import treeService from "../services/tree.js";
import openService from "./open.js";
export default class DialogCommandExecutor extends Component { export default class RootCommandExecutor extends Component {
jumpToNoteCommand() { jumpToNoteCommand() {
import("../dialogs/jump_to_note.js").then(d => d.showDialog()); import("../dialogs/jump_to_note.js").then(d => d.showDialog());
} }
@ -84,4 +85,12 @@ export default class DialogCommandExecutor extends Component {
showBackendLogCommand() { showBackendLogCommand() {
import("../dialogs/backend_log.js").then(d => d.showDialog()); import("../dialogs/backend_log.js").then(d => d.showDialog());
} }
openNoteExternallyCommand() {
const noteId = appContext.tabManager.getActiveTabNoteId();
if (noteId) {
openService.openNoteExternally(noteId);
}
}
} }

View File

@ -127,7 +127,7 @@ async function sortAlphabetically(noteId) {
} }
ws.subscribeToMessages(message => { ws.subscribeToMessages(message => {
if (message.type === 'open-note') { if (message.type === 'openNote') {
appContext.tabManager.activateOrOpenNote(message.noteId); appContext.tabManager.activateOrOpenNote(message.noteId);
if (utils.isElectron()) { if (utils.isElectron()) {

View File

@ -59,7 +59,7 @@ async function handleMessage(event) {
} }
if (message.type === 'frontend-update') { if (message.type === 'frontend-update') {
let {entityChanges, lastSyncedPush} = message.data; let {entityChanges} = message.data;
lastPingTs = Date.now(); lastPingTs = Date.now();
if (entityChanges.length > 0) { if (entityChanges.length > 0) {

View File

@ -22,6 +22,7 @@ const TPL = `
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
position: relative; position: relative;
top: -2px;
border-radius: 0; border-radius: 0;
} }

View File

@ -1,11 +1,12 @@
import TabAwareWidget from "./tab_aware_widget.js"; import TabAwareWidget from "./tab_aware_widget.js";
import protectedSessionService from "../services/protected_session.js"; import protectedSessionService from "../services/protected_session.js";
import utils from "../services/utils.js";
const TPL = ` const TPL = `
<div class="dropdown note-actions"> <div class="dropdown note-actions">
<style> <style>
.note-actions .dropdown-menu { .note-actions .dropdown-menu {
width: 15em; width: 20em;
} }
.note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover { .note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover {
@ -84,6 +85,7 @@ const TPL = `
<a data-trigger-command="showNoteRevisions" class="dropdown-item show-note-revisions-button">Revisions</a> <a data-trigger-command="showNoteRevisions" class="dropdown-item show-note-revisions-button">Revisions</a>
<a data-trigger-command="showLinkMap" class="dropdown-item show-link-map-button"><kbd data-command="showLinkMap"></kbd> Link map</a> <a data-trigger-command="showLinkMap" class="dropdown-item show-link-map-button"><kbd data-command="showLinkMap"></kbd> Link map</a>
<a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> Note source</a> <a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> Note source</a>
<a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"><kbd data-command="openNoteExternally"></kbd> Open note externally</a>
<a class="dropdown-item import-files-button">Import files</a> <a class="dropdown-item import-files-button">Import files</a>
<a class="dropdown-item export-note-button">Export note</a> <a class="dropdown-item export-note-button">Export note</a>
<a data-trigger-command="printActiveNote" class="dropdown-item print-note-button"><kbd data-command="printActiveNote"></kbd> Print note</a> <a data-trigger-command="printActiveNote" class="dropdown-item print-note-button"><kbd data-command="printActiveNote"></kbd> Print note</a>
@ -119,6 +121,8 @@ export default class NoteActionsWidget extends TabAwareWidget {
this.$widget.on('click', '.dropdown-item', this.$widget.on('click', '.dropdown-item',
() => this.$widget.find('.dropdown-toggle').dropdown('toggle')); () => this.$widget.find('.dropdown-toggle').dropdown('toggle'));
this.$openNoteExternallyButton = this.$widget.find(".open-note-externally-button");
} }
refreshWithNote(note) { refreshWithNote(note) {
@ -128,6 +132,8 @@ export default class NoteActionsWidget extends TabAwareWidget {
this.$protectButton.toggle(!note.isProtected); this.$protectButton.toggle(!note.isProtected);
this.$unprotectButton.toggle(!!note.isProtected); this.$unprotectButton.toggle(!!note.isProtected);
this.$openNoteExternallyButton.toggle(utils.isElectron());
} }
toggleDisabled($el, enable) { toggleDisabled($el, enable) {

View File

@ -382,8 +382,6 @@ export default class NoteTreeWidget extends TabAwareWidget {
} }
else { else {
node.setActive(); node.setActive();
this.clearSelectedNodes();
} }
return false; return false;
@ -393,6 +391,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
// click event won't propagate so let's close context menu manually // click event won't propagate so let's close context menu manually
contextMenu.hide(); contextMenu.hide();
this.clearSelectedNodes();
const notePath = treeService.getNotePath(data.node); const notePath = treeService.getNotePath(data.node);
const activeTabContext = appContext.tabManager.getActiveTabContext(); const activeTabContext = appContext.tabManager.getActiveTabContext();
@ -1144,11 +1144,12 @@ export default class NoteTreeWidget extends TabAwareWidget {
} }
if (node) { if (node) {
node.setActive(true, {noEvents: true, noFocus: !activeNodeFocused});
if (activeNodeFocused) { if (activeNodeFocused) {
node.setFocus(true); // needed by Firefox: https://github.com/zadam/trilium/issues/1865
this.tree.$container.focus();
} }
await node.setActive(true, {noEvents: true, noFocus: !activeNodeFocused});
} }
else { else {
// this is used when original note has been deleted and we want to move the focus to the note above/below // this is used when original note has been deleted and we want to move the focus to the note above/below

View File

@ -0,0 +1,64 @@
import TabAwareWidget from "./tab_aware_widget.js";
import server from "../services/server.js";
import fileWatcher from "../services/file_watcher.js";
const TPL = `
<div class="dropdown note-update-status-widget alert alert-warning">
<style>
.note-update-status-widget {
margin: 10px;
}
</style>
<p>File <code class="file-path"></code> has been last modified on <span class="file-last-modified"></span>.</p>
<div style="display: flex; flex-direction: row; justify-content: space-evenly;">
<button class="btn btn-sm file-upload-button">Upload modified file</button>
<button class="btn btn-sm ignore-this-change-button">Ignore this change</button>
</div>
</div>`;
export default class NoteUpdateStatusWidget extends TabAwareWidget {
isEnabled() {
return super.isEnabled()
&& !!fileWatcher.getFileModificationStatus(this.noteId);
}
doRender() {
this.$widget = $(TPL);
this.overflowing();
this.$filePath = this.$widget.find(".file-path");
this.$fileLastModified = this.$widget.find(".file-last-modified");
this.$fileUploadButton = this.$widget.find(".file-upload-button");
this.$fileUploadButton.on("click", async () => {
await server.post(`notes/${this.noteId}/upload-modified-file`, {
filePath: this.$filePath.text()
});
fileWatcher.fileModificationUploaded(this.noteId);
this.refresh();
});
this.$ignoreThisChangeButton = this.$widget.find(".ignore-this-change-button");
this.$ignoreThisChangeButton.on('click', () => {
fileWatcher.ignoreModification(this.noteId);
this.refresh();
});
}
refreshWithNote(note) {
const status = fileWatcher.getFileModificationStatus(note.noteId);
this.$filePath.text(status.filePath);
this.$fileLastModified.text(dayjs.unix(status.lastModifiedMs / 1000).format("HH:mm:ss"));
}
openedFileUpdatedEvent(data) {
if (data.noteId === this.noteId) {
this.refresh();
}
}
}

View File

@ -5,7 +5,7 @@ const TPL = `
<td colspan="2"> <td colspan="2">
<span class="bx bx-trash"></span> <span class="bx bx-trash"></span>
Delete matched note Delete matched notes
</td> </td>
<td class="button-column"> <td class="button-column">
<span class="bx bx-x icon-action action-conf-del"></span> <span class="bx bx-x icon-action action-conf-del"></span>

View File

@ -21,6 +21,7 @@ const TPL = `
<option value="ownedLabelCount">Number of labels</option> <option value="ownedLabelCount">Number of labels</option>
<option value="ownedRelationCount">Number of relations</option> <option value="ownedRelationCount">Number of relations</option>
<option value="targetRelationCount">Number of relations targeting the note</option> <option value="targetRelationCount">Number of relations targeting the note</option>
<option value="random">Random order</option>
</select> </select>
<select name="orderDirection" class="form-control w-auto d-inline"> <select name="orderDirection" class="form-control w-auto d-inline">

View File

@ -82,7 +82,7 @@ export default class FilePropertiesWidget extends TabAwareWidget {
this.$uploadNewRevisionInput = this.$widget.find(".file-upload-new-revision-input"); this.$uploadNewRevisionInput = this.$widget.find(".file-upload-new-revision-input");
this.$downloadButton.on('click', () => openService.downloadFileNote(this.noteId)); this.$downloadButton.on('click', () => openService.downloadFileNote(this.noteId));
this.$openButton.on('click', () => openService.openFileNote(this.noteId)); this.$openButton.on('click', () => openService.openNoteExternally(this.noteId));
this.$uploadNewRevisionButton.on("click", () => { this.$uploadNewRevisionButton.on("click", () => {
this.$uploadNewRevisionInput.trigger("click"); this.$uploadNewRevisionInput.trigger("click");

View File

@ -26,6 +26,8 @@ const TPL = `
<div class="no-print" style="display: flex; justify-content: space-evenly; margin: 10px;"> <div class="no-print" style="display: flex; justify-content: space-evenly; margin: 10px;">
<button class="image-download btn btn-sm btn-primary" type="button">Download</button> <button class="image-download btn btn-sm btn-primary" type="button">Download</button>
<button class="image-open btn btn-sm btn-primary" type="button">Open</button>
<button class="image-copy-to-clipboard btn btn-sm btn-primary" type="button">Copy to clipboard</button> <button class="image-copy-to-clipboard btn btn-sm btn-primary" type="button">Copy to clipboard</button>
<button class="image-upload-new-revision btn btn-sm btn-primary" type="button">Upload new revision</button> <button class="image-upload-new-revision btn btn-sm btn-primary" type="button">Upload new revision</button>
@ -59,6 +61,9 @@ export default class ImagePropertiesWidget extends TabAwareWidget {
this.$fileType = this.$widget.find(".image-filetype"); this.$fileType = this.$widget.find(".image-filetype");
this.$fileSize = this.$widget.find(".image-filesize"); this.$fileSize = this.$widget.find(".image-filesize");
this.$openButton = this.$widget.find(".image-open");
this.$openButton.on('click', () => openService.openNoteExternally(this.noteId));
this.$imageDownloadButton = this.$widget.find(".image-download"); this.$imageDownloadButton = this.$widget.find(".image-download");
this.$imageDownloadButton.on('click', () => openService.downloadFileNote(this.noteId)); this.$imageDownloadButton.on('click', () => openService.downloadFileNote(this.noteId));

View File

@ -1,6 +1,7 @@
import utils from "../../services/utils.js";
import openService from "../../services/open.js"; import openService from "../../services/open.js";
import TypeWidget from "./type_widget.js"; import TypeWidget from "./type_widget.js";
import fileWatcher from "../../services/file_watcher.js";
import server from "../../services/server.js";
const TPL = ` const TPL = `
<div class="note-detail-file note-detail-printable"> <div class="note-detail-file note-detail-printable">
@ -50,9 +51,6 @@ export default class FileTypeWidget extends TypeWidget {
} }
async doRefresh(note) { async doRefresh(note) {
const attributes = note.getAttributes();
const attributeMap = utils.toObject(attributes, l => [l.name, l.value]);
this.$widget.show(); this.$widget.show();
const noteComplement = await this.tabContext.getNoteComplement(); const noteComplement = await this.tabContext.getNoteComplement();
@ -73,14 +71,14 @@ export default class FileTypeWidget extends TypeWidget {
else if (note.mime.startsWith('video/')) { else if (note.mime.startsWith('video/')) {
this.$videoPreview this.$videoPreview
.show() .show()
.attr("src", openService.getUrlForDownload("api/notes/" + this.noteId + "/open")) .attr("src", openService.getUrlForDownload("api/notes/" + this.noteId + "/open-partial"))
.attr("type", this.note.mime) .attr("type", this.note.mime)
.css("width", this.$widget.width()); .css("width", this.$widget.width());
} }
else if (note.mime.startsWith('audio/')) { else if (note.mime.startsWith('audio/')) {
this.$audioPreview this.$audioPreview
.show() .show()
.attr("src", openService.getUrlForDownload("api/notes/" + this.noteId + "/open")) .attr("src", openService.getUrlForDownload("api/notes/" + this.noteId + "/open-partial"))
.attr("type", this.note.mime) .attr("type", this.note.mime)
.css("width", this.$widget.width()); .css("width", this.$widget.width());
} }

View File

@ -164,11 +164,6 @@ div.ui-tooltip {
overflow: auto; overflow: auto;
} }
.alert {
padding: 5px;
width: auto;
}
/* /*
* .search-inactive is added to search window <webview> when the window * .search-inactive is added to search window <webview> when the window
* is inactive. * is inactive.
@ -761,9 +756,14 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
width: 100%; width: 100%;
} }
.alert {
padding: 8px 14px;
width: auto;
}
.alert-warning, .alert-info { .alert-warning, .alert-info {
color: var(--main-text-color) !important; color: var(--main-text-color) !important;
background-color: var(--accented-background-color) !important; background-color: transparent !important;
border-color: var(--main-border-color) !important; border-color: var(--main-border-color) !important;
} }

View File

@ -107,7 +107,11 @@ function processContent(images, note, content) {
const filename = path.basename(src); const filename = path.basename(src);
if (!dataUrl || !dataUrl.startsWith("data:image")) { if (!dataUrl || !dataUrl.startsWith("data:image")) {
log.info("Image could not be recognized as data URL:", dataUrl.substr(0, Math.min(100, dataUrl.length))); const excerpt = dataUrl
? dataUrl.substr(0, Math.min(100, dataUrl.length))
: "null";
log.info("Image could not be recognized as data URL: " + excerpt);
continue; continue;
} }
@ -140,7 +144,7 @@ function processContent(images, note, content) {
function openNote(req) { function openNote(req) {
if (utils.isElectron()) { if (utils.isElectron()) {
ws.sendMessageToAllClients({ ws.sendMessageToAllClients({
type: 'open-note', type: 'openNote',
noteId: req.params.noteId noteId: req.params.noteId
}); });

View File

@ -3,10 +3,13 @@
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');
const log = require('../../services/log');
const noteRevisionService = require('../../services/note_revisions'); const noteRevisionService = require('../../services/note_revisions');
const tmp = require('tmp'); const tmp = require('tmp');
const fs = require('fs'); const fs = require('fs');
const { Readable } = require('stream'); const { Readable } = require('stream');
const chokidar = require('chokidar');
const ws = require('../../services/ws');
function updateFile(req) { function updateFile(req) {
const {noteId} = req.params; const {noteId} = req.params;
@ -120,6 +123,19 @@ function saveToTmpDir(req) {
fs.writeSync(tmpObj.fd, note.getContent()); fs.writeSync(tmpObj.fd, note.getContent());
fs.closeSync(tmpObj.fd); fs.closeSync(tmpObj.fd);
log.info(`Saved temporary file for note ${noteId} into ${tmpObj.name}`);
if (utils.isElectron()) {
chokidar.watch(tmpObj.name).on('change', (path, stats) => {
ws.sendMessageToAllClients({
type: 'openedFileUpdated',
noteId: noteId,
lastModifiedMs: stats.atimeMs,
filePath: tmpObj.name
});
});
}
return { return {
tmpFilePath: tmpObj.name tmpFilePath: tmpObj.name
}; };

View File

@ -7,6 +7,8 @@ const sql = require('../../services/sql');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const log = require('../../services/log'); const log = require('../../services/log');
const TaskContext = require('../../services/task_context'); const TaskContext = require('../../services/task_context');
const fs = require('fs');
const noteRevisionService = require("../../services/note_revisions.js");
function getNote(req) { function getNote(req) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
@ -80,7 +82,7 @@ function deleteNote(req) {
function undeleteNote(req) { function undeleteNote(req) {
const note = repository.getNote(req.params.noteId); const note = repository.getNote(req.params.noteId);
const taskContext = TaskContext.getInstance(utils.randomString(10), 'undelete-notes'); const taskContext = TaskContext.getInstance(utils.randomString(10), 'undeleteNotes');
noteService.undeleteNote(note, note.deleteId, taskContext); noteService.undeleteNote(note, note.deleteId, taskContext);
@ -109,7 +111,7 @@ function protectNote(req) {
const protect = !!parseInt(req.params.isProtected); const protect = !!parseInt(req.params.isProtected);
const includingSubTree = !!parseInt(req.query.subtree); const includingSubTree = !!parseInt(req.query.subtree);
const taskContext = new TaskContext(utils.randomString(10), 'protect-notes', {protect}); const taskContext = new TaskContext(utils.randomString(10), 'protectNotes', {protect});
noteService.protectNoteRecursively(note, protect, includingSubTree, taskContext); noteService.protectNoteRecursively(note, protect, includingSubTree, taskContext);
@ -273,6 +275,29 @@ function getDeleteNotesPreview(req) {
}; };
} }
function uploadModifiedFile(req) {
const noteId = req.params.noteId;
const {filePath} = req.body;
const note = repository.getNote(noteId);
if (!note) {
return [404, `Note ${noteId} has not been found`];
}
log.info(`Updating note ${noteId} with content from ${filePath}`);
noteRevisionService.createNoteRevision(note);
const fileContent = fs.readFileSync(filePath);
if (!fileContent) {
return [400, `File ${fileContent} is empty`];
}
note.setContent(fileContent);
}
module.exports = { module.exports = {
getNote, getNote,
updateNote, updateNote,
@ -286,5 +311,6 @@ module.exports = {
changeTitle, changeTitle,
duplicateSubtree, duplicateSubtree,
eraseDeletedNotesNow, eraseDeletedNotesNow,
getDeleteNotesPreview getDeleteNotesPreview,
uploadModifiedFile
}; };

View File

@ -3,6 +3,7 @@
const utils = require('../services/utils'); const utils = require('../services/utils');
const optionService = require('../services/options'); const optionService = require('../services/options');
const myScryptService = require('../services/my_scrypt'); const myScryptService = require('../services/my_scrypt');
const log = require('../services/log');
function loginPage(req, res) { function loginPage(req, res) {
res.render('login', { failedAuth: false }); res.render('login', { failedAuth: false });
@ -28,6 +29,9 @@ function login(req, res) {
}); });
} }
else { else {
// note that logged IP address is usually meaningless since the traffic should come from a reverse proxy
log.info(`WARNING: Wrong username / password from ${req.ip}, rejecting.`);
res.render('login', {'failedAuth': true}); res.render('login', {'failedAuth': true});
} }
} }

View File

@ -165,6 +165,7 @@ function register(app) {
apiRoute(POST, '/api/notes/erase-deleted-notes-now', notesApiRoute.eraseDeletedNotesNow); apiRoute(POST, '/api/notes/erase-deleted-notes-now', notesApiRoute.eraseDeletedNotesNow);
apiRoute(PUT, '/api/notes/:noteId/change-title', notesApiRoute.changeTitle); apiRoute(PUT, '/api/notes/:noteId/change-title', notesApiRoute.changeTitle);
apiRoute(POST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateSubtree); apiRoute(POST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateSubtree);
apiRoute(POST, '/api/notes/:noteId/upload-modified-file', notesApiRoute.uploadModifiedFile);
apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate); apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate);
@ -177,14 +178,15 @@ function register(app) {
route(PUT, '/api/notes/:noteId/file', [auth.checkApiAuthOrElectron, uploadMiddleware, csrfMiddleware], route(PUT, '/api/notes/:noteId/file', [auth.checkApiAuthOrElectron, uploadMiddleware, csrfMiddleware],
filesRoute.updateFile, apiResultHandler); filesRoute.updateFile, apiResultHandler);
route(GET, '/api/notes/:noteId/open', [auth.checkApiAuthOrElectron], route(GET, '/api/notes/:noteId/open', [auth.checkApiAuthOrElectron], filesRoute.openFile);
route(GET, '/api/notes/:noteId/open-partial', [auth.checkApiAuthOrElectron],
createPartialContentHandler(filesRoute.fileContentProvider, { createPartialContentHandler(filesRoute.fileContentProvider, {
debug: (string, extra) => { console.log(string, extra); } debug: (string, extra) => { console.log(string, extra); }
})); }));
route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile);
// this "hacky" path is used for easier referencing of CSS resources // this "hacky" path is used for easier referencing of CSS resources
route(GET, '/api/notes/download/:noteId', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); route(GET, '/api/notes/download/:noteId', [auth.checkApiAuthOrElectron], filesRoute.downloadFile);
apiRoute(POST, '/api/notes/:noteId/saveToTmpDir', filesRoute.saveToTmpDir); apiRoute(POST, '/api/notes/:noteId/save-to-tmp-dir', filesRoute.saveToTmpDir);
apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getEffectiveNoteAttributes); apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getEffectiveNoteAttributes);
apiRoute(POST, '/api/notes/:noteId/attributes', attributesRoute.addNoteAttribute); apiRoute(POST, '/api/notes/:noteId/attributes', attributesRoute.addNoteAttribute);

Binary file not shown.

View File

@ -1 +1 @@
module.exports = { buildDate:"2021-04-11T22:29:56+02:00", buildRevision: "58e4bd4974275a113c50e4ed7a554987921d55fc" }; module.exports = { buildDate:"2021-04-19T22:43:03+02:00", buildRevision: "6136243d6117910b80feafad4fc7121ecc42d794" };

View File

@ -36,6 +36,12 @@ async function importOpml(taskContext, fileBuffer, parentNote) {
if (opmlVersion === 1) { if (opmlVersion === 1) {
title = outline.$.title; title = outline.$.title;
content = toHtml(outline.$.text); content = toHtml(outline.$.text);
if (!title || !title.trim()) {
// https://github.com/zadam/trilium/issues/1862
title = outline.$.text;
content = '';
}
} }
else if (opmlVersion === 2) { else if (opmlVersion === 2) {
title = outline.$.text; title = outline.$.text;

View File

@ -352,6 +352,12 @@ const DEFAULT_KEYBOARD_ACTIONS = [
defaultShortcuts: [], defaultShortcuts: [],
scope: "window" scope: "window"
}, },
{
actionName: "openNoteExternally",
defaultShortcuts: [],
description: "Open note as a file with default application",
scope: "window"
},
{ {
actionName: "renderActiveNote", actionName: "renderActiveNote",
defaultShortcuts: [], defaultShortcuts: [],

View File

@ -1,6 +1,7 @@
"use strict"; "use strict";
const Expression = require('./expression'); const Expression = require('./expression');
const TrueExp = require("./true.js");
class AndExp extends Expression { class AndExp extends Expression {
static of(subExpressions) { static of(subExpressions) {
@ -10,6 +11,8 @@ class AndExp extends Expression {
return subExpressions[0]; return subExpressions[0];
} else if (subExpressions.length > 0) { } else if (subExpressions.length > 0) {
return new AndExp(subExpressions); return new AndExp(subExpressions);
} else {
return new TrueExp();
} }
} }

View File

@ -2,6 +2,7 @@
const Expression = require('./expression'); const Expression = require('./expression');
const NoteSet = require('../note_set'); const NoteSet = require('../note_set');
const TrueExp = require("./true");
class OrExp extends Expression { class OrExp extends Expression {
static of(subExpressions) { static of(subExpressions) {
@ -13,6 +14,9 @@ class OrExp extends Expression {
else if (subExpressions.length > 0) { else if (subExpressions.length > 0) {
return new OrExp(subExpressions); return new OrExp(subExpressions);
} }
else {
return new TrueExp();
}
} }
constructor(subExpressions) { constructor(subExpressions) {

View File

@ -28,17 +28,33 @@ class OrderByAndLimitExp extends Expression {
let valA = valueExtractor.extract(a); let valA = valueExtractor.extract(a);
let valB = valueExtractor.extract(b); let valB = valueExtractor.extract(b);
if (!isNaN(valA) && !isNaN(valB)) { if (valA === null && valB === null) {
// neither has attribute at all
continue;
}
else if (valB === null) {
return smaller;
}
else if (valA === null) {
return larger;
}
// if both are numbers then parse them for numerical comparison
// beware that isNaN will return false for empty string and null
if (valA.trim() !== "" && valB.trim() !== "" && !isNaN(valA) && !isNaN(valB)) {
valA = parseFloat(valA); valA = parseFloat(valA);
valB = parseFloat(valB); valB = parseFloat(valB);
} }
if (valA < valB) { if (!valA && !valB) {
// the attribute is not defined in either note so continue to next order definition
continue;
} else if (!valB || valA < valB) {
return smaller; return smaller;
} else if (valA > valB) { } else if (!valA || valA > valB) {
return larger; return larger;
} }
// else go to next order definition // else the values are equal and continue to next order definition
} }
return 0; return 0;

View File

@ -0,0 +1,11 @@
"use strict";
const Expression = require('./expression');
class TrueExp extends Expression {
execute(inputNoteSet, executionContext) {
return inputNoteSet;
}
}
module.exports = TrueExp;

View File

@ -329,6 +329,9 @@ function getExpression(tokens, searchContext, level = 0) {
else if (op === 'or') { else if (op === 'or') {
return OrExp.of(expressions); return OrExp.of(expressions);
} }
else {
throw new Error(`Unrecognized op=${op}`);
}
} }
for (i = 0; i < tokens.length; i++) { for (i = 0; i < tokens.length; i++) {
@ -358,8 +361,7 @@ function getExpression(tokens, searchContext, level = 0) {
continue; continue;
} }
exp.subExpression = getAggregateExpression(); exp.subExpression = getAggregateExpression();console.log(exp);
return exp; return exp;
} }
else if (token === 'not') { else if (token === 'not') {

View File

@ -69,7 +69,7 @@ class ValueExtractor {
i++; i++;
} }
else if (pathEl in PROP_MAPPING) { else if (pathEl in PROP_MAPPING || pathEl === 'random') {
if (i !== this.propertyPath.length - 1) { if (i !== this.propertyPath.length - 1) {
return `${pathEl} is a terminal property specifier and must be at the end`; return `${pathEl} is a terminal property specifier and must be at the end`;
} }
@ -113,6 +113,9 @@ class ValueExtractor {
else if (cur() === 'children') { else if (cur() === 'children') {
cursor = cursor.children[0]; cursor = cursor.children[0];
} }
else if (cur() === 'random') {
return Math.random();
}
else if (cur() in PROP_MAPPING) { else if (cur() in PROP_MAPPING) {
return cursor[PROP_MAPPING[cur()]]; return cursor[PROP_MAPPING[cur()]];
} }

View File

@ -59,6 +59,7 @@ async function sync() {
if (e.message && if (e.message &&
(e.message.includes('ECONNREFUSED') || (e.message.includes('ECONNREFUSED') ||
e.message.includes('ERR_CONNECTION_REFUSED') || e.message.includes('ERR_CONNECTION_REFUSED') ||
e.message.includes('ERR_ADDRESS_UNREACHABLE') ||
e.message.includes('Bad Gateway'))) { e.message.includes('Bad Gateway'))) {
ws.syncFailed(); ws.syncFailed();

View File

@ -38,7 +38,7 @@ class TaskContext {
this.lastSentCountTs = Date.now(); this.lastSentCountTs = Date.now();
ws.sendMessageToAllClients({ ws.sendMessageToAllClients({
type: 'task-progress-count', type: 'taskProgressCount',
taskId: this.taskId, taskId: this.taskId,
taskType: this.taskType, taskType: this.taskType,
data: this.data, data: this.data,
@ -49,7 +49,7 @@ class TaskContext {
reportError(message) { reportError(message) {
ws.sendMessageToAllClients({ ws.sendMessageToAllClients({
type: 'task-error', type: 'taskError',
taskId: this.taskId, taskId: this.taskId,
taskType: this.taskType, taskType: this.taskType,
data: this.data, data: this.data,
@ -59,7 +59,7 @@ class TaskContext {
taskSucceeded(result) { taskSucceeded(result) {
ws.sendMessageToAllClients({ ws.sendMessageToAllClients({
type: 'task-succeeded', type: 'taskSucceeded',
taskId: this.taskId, taskId: this.taskId,
taskType: this.taskType, taskType: this.taskType,
data: this.data, data: this.data,

View File

@ -188,6 +188,8 @@ function formatDownloadTitle(filename, type, mime) {
filename = "untitled"; filename = "untitled";
} }
filename = sanitize(filename);
if (type === 'text') { if (type === 'text') {
return filename + '.html'; return filename + '.html';
} else if (['relation-map', 'search'].includes(type)) { } else if (['relation-map', 'search'].includes(type)) {

View File

@ -18,12 +18,12 @@
<ul> <ul>
<li><kbd>UP</kbd>, <kbd>DOWN</kbd> - go up/down in the list of notes</li> <li><kbd>UP</kbd>, <kbd>DOWN</kbd> - go up/down in the list of notes</li>
<li><kbd>LEFT</kbd>, <kbd>RIGHT</kbd> - collapse/expand node</li> <li><kbd>LEFT</kbd>, <kbd>RIGHT</kbd> - collapse/expand node</li>
<li><kbd data-command="backInNoteHistory"></kbd>, <kbd data-command="forwardInNoteHistory"></kbd> - go back / forwards in the history</li> <li><kbd data-command="backInNoteHistory">not set</kbd>, <kbd data-command="forwardInNoteHistory">not set</kbd> - go back / forwards in the history</li>
<li><kbd data-command="jumpToNote"></kbd> - show <a class="external" href="https://github.com/zadam/trilium/wiki/Note-navigation#jump-to-note">"Jump to" dialog</a></li> <li><kbd data-command="jumpToNote">not set</kbd> - show <a class="external" href="https://github.com/zadam/trilium/wiki/Note-navigation#jump-to-note">"Jump to" dialog</a></li>
<li><kbd data-command="scrollToActiveNote"></kbd> - scroll to active note</li> <li><kbd data-command="scrollToActiveNote">not set</kbd> - scroll to active note</li>
<li><kbd>Backspace</kbd> - jump to parent note</li> <li><kbd>Backspace</kbd> - jump to parent note</li>
<li><kbd data-command="collapseTree"></kbd> - collapse whole note tree</li> <li><kbd data-command="collapseTree">not set</kbd> - collapse whole note tree</li>
<li><kbd data-command="collapseSubtree"></kbd> - collapse sub-tree</li> <li><kbd data-command="collapseSubtree">not set</kbd> - collapse sub-tree</li>
</ul> </ul>
</p> </p>
</div> </div>
@ -40,10 +40,10 @@
Only in desktop (electron build): Only in desktop (electron build):
<ul> <ul>
<li><kbd data-command="openNewTab"></kbd> open empty tab</li> <li><kbd data-command="openNewTab">not set</kbd> open empty tab</li>
<li><kbd data-command="closeActiveTab"></kbd> close active tab</li> <li><kbd data-command="closeActiveTab">not set</kbd> close active tab</li>
<li><kbd data-command="activateNextTab"></kbd> activate next tab</li> <li><kbd data-command="activateNextTab">not set</kbd> activate next tab</li>
<li><kbd data-command="activatePreviousTab"></kbd> activate previous tab</li> <li><kbd data-command="activatePreviousTab">not set</kbd> activate previous tab</li>
</ul> </ul>
</p> </p>
</div> </div>
@ -55,9 +55,9 @@
<p class="card-text"> <p class="card-text">
<ul> <ul>
<li><kbd data-command="createNoteAfter"></kbd> - create new note after the active note</li> <li><kbd data-command="createNoteAfter">not set</kbd> - create new note after the active note</li>
<li><kbd data-command="createNoteInto"></kbd> - create new sub-note into active note</li> <li><kbd data-command="createNoteInto">not set</kbd> - create new sub-note into active note</li>
<li><kbd data-command="editBranchPrefix"></kbd> - edit <a class="external" href="https://github.com/zadam/trilium/wiki/Tree concepts#prefix">prefix</a> of active note clone</li> <li><kbd data-command="editBranchPrefix">not set</kbd> - edit <a class="external" href="https://github.com/zadam/trilium/wiki/Tree concepts#prefix">prefix</a> of active note clone</li>
</ul> </ul>
</p> </p>
</div> </div>
@ -69,15 +69,15 @@
<p class="card-text"> <p class="card-text">
<ul> <ul>
<li><kbd data-command="moveNoteUp"></kbd>, <kbd data-command="moveNoteDown"></kbd> - move note up/down in the note list</li> <li><kbd data-command="moveNoteUp">not set</kbd>, <kbd data-command="moveNoteDown">not set</kbd> - move note up/down in the note list</li>
<li><kbd data-command="moveNoteUpInHierarchy"></kbd>, <kbd data-command="moveNoteDownInHierarchy"></kbd> - move note up in the hierarchy</li> <li><kbd data-command="moveNoteUpInHierarchy">not set</kbd>, <kbd data-command="moveNoteDownInHierarchy">not set</kbd> - move note up in the hierarchy</li>
<li><kbd data-command="addNoteAboveToSelection"></kbd>, <kbd data-command="addNoteBelowToSelection"></kbd> - multi-select note above/below</li> <li><kbd data-command="addNoteAboveToSelection">not set</kbd>, <kbd data-command="addNoteBelowToSelection">not set</kbd> - multi-select note above/below</li>
<li><kbd data-command="selectAllNotesInParent"></kbd> - select all notes in the current level</li> <li><kbd data-command="selectAllNotesInParent">not set</kbd> - select all notes in the current level</li>
<li><kbd>Shift+click</kbd> - select note</li> <li><kbd>Shift+click</kbd> - select note</li>
<li><kbd data-command="copyNotesToClipboard"></kbd> - copy active note (or current selection) into clipboard (used for <a class="external" href="https://github.com/zadam/trilium/wiki/Cloning notes">cloning</a>)</li> <li><kbd data-command="copyNotesToClipboard">not set</kbd> - copy active note (or current selection) into clipboard (used for <a class="external" href="https://github.com/zadam/trilium/wiki/Cloning notes">cloning</a>)</li>
<li><kbd data-command="cutNotesToClipboard"></kbd> - cut current (or current selection) note into clipboard (used for moving notes)</li> <li><kbd data-command="cutNotesToClipboard">not set</kbd> - cut current (or current selection) note into clipboard (used for moving notes)</li>
<li><kbd data-command="pasteNotesFromClipboard"></kbd> - paste note(s) as sub-note into active note (which is either move or clone depending on whether it was copied or cut into clipboard)</li> <li><kbd data-command="pasteNotesFromClipboard">not set</kbd> - paste note(s) as sub-note into active note (which is either move or clone depending on whether it was copied or cut into clipboard)</li>
<li><kbd data-command="deleteNotes"></kbd> - delete note / sub-tree</li> <li><kbd data-command="deleteNotes">not set</kbd> - delete note / sub-tree</li>
</ul> </ul>
</p> </p>
</div> </div>
@ -89,12 +89,12 @@
<p class="card-text"> <p class="card-text">
<ul> <ul>
<li><kbd data-command="editNoteTitle"></kbd> in tree pane will switch from tree pane into note title. Enter from note title will switch focus to text editor. <li><kbd data-command="editNoteTitle">not set</kbd> in tree pane will switch from tree pane into note title. Enter from note title will switch focus to text editor.
<kbd data-command="scrollToActiveNote"></kbd> will switch back from editor to tree pane.</li> <kbd data-command="scrollToActiveNote">not set</kbd> will switch back from editor to tree pane.</li>
<li><kbd>Ctrl+K</kbd> - create / edit external link</li> <li><kbd>Ctrl+K</kbd> - create / edit external link</li>
<li><kbd data-command="addLinkToText"></kbd> - create internal link</li> <li><kbd data-command="addLinkToText">not set</kbd> - create internal link</li>
<li><kbd data-command="insertDateTimeToText"></kbd> - insert current date and time at caret position</li> <li><kbd data-command="insertDateTimeToText">not set</kbd> - insert current date and time at caret position</li>
<li><kbd data-command="scrollToActiveNote"></kbd> - jump away to the tree pane and scroll to active note</li> <li><kbd data-command="scrollToActiveNote">not set</kbd> - jump away to the tree pane and scroll to active note</li>
</ul> </ul>
</p> </p>
</div> </div>
@ -121,9 +121,9 @@
<p class="card-text"> <p class="card-text">
<ul> <ul>
<li><kbd data-command="reloadFrontendApp"></kbd> - reload Trilium frontend</li> <li><kbd data-command="reloadFrontendApp">not set</kbd> - reload Trilium frontend</li>
<li><kbd data-command="openDevTools"></kbd> - show developer tools</li> <li><kbd data-command="openDevTools">not set</kbd> - show developer tools</li>
<li><kbd data-command="showSQLConsole"></kbd> - show SQL console</li> <li><kbd data-command="showSQLConsole">not set</kbd> - show SQL console</li>
</ul> </ul>
</p> </p>
</div> </div>
@ -135,9 +135,9 @@
<p class="card-text"> <p class="card-text">
<ul> <ul>
<li><kbd data-command="toggleZenMode"></kbd> - Zen mode - display only note editor, everything else is hidden</li> <li><kbd data-command="toggleZenMode">not set</kbd> - Zen mode - display only note editor, everything else is hidden</li>
<li><kbd data-command="searchNotes"></kbd> - toggle search form in tree pane</li> <li><kbd data-command="quickSearch">not set</kbd> - focus on quick search input</li>
<li><kbd data-command="findInText"></kbd> - in page search</li> <li><kbd data-command="findInText">not set</kbd> - in page search</li>
</ul> </ul>
</p> </p>
</div> </div>

11
src/www
View File

@ -8,10 +8,13 @@ process.on('unhandledRejection', error => {
require('./services/log').info(error); require('./services/log').info(error);
}); });
process.on('SIGINT', function() { function exit() {
console.log("Caught interrupt signal. Exiting."); console.log("Caught interrupt/termination signal. Exiting.");
process.exit(); process.exit(0);
}); }
process.on('SIGINT', exit);
process.on('SIGTERM', exit);
const { app, sessionParser } = require('./app'); const { app, sessionParser } = require('./app');
const fs = require('fs'); const fs = require('fs');

View File

@ -10,9 +10,12 @@
<excludeFolder url="file://$MODULE_DIR$/dist" /> <excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/src/public/app-dist" /> <excludeFolder url="file://$MODULE_DIR$/src/public/app-dist" />
<excludeFolder url="file://$MODULE_DIR$/libraries" /> <excludeFolder url="file://$MODULE_DIR$/libraries" />
<excludeFolder url="file://$MODULE_DIR$/libraries" />
<excludeFolder url="file://$MODULE_DIR$/docs" />
<excludeFolder url="file://$MODULE_DIR$/bin/better-sqlite3" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="@types/jquery" level="application" /> <orderEntry type="library" name="@types/jquery" level="application" />
</component> </component>
</module> </module>