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

1
.idea/vcs.xml generated
View File

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

View File

@ -10,7 +10,7 @@ fi
cd dist
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
cd ..

View File

@ -5,7 +5,7 @@ if [[ $# -eq 0 ]] ; then
exit 1
fi
npm run webpack
n exec 12 npm run webpack
DIR=$1
@ -27,7 +27,7 @@ cp -r electron.js $DIR/
cp webpack-* $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
rm -r $DIR/node_modules/image-q/demo

View File

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

View File

@ -55,47 +55,20 @@ echo "Creating release in GitHub"
EXTRA=
if [[ $TAG == *"beta"* ]]; then
EXTRA=--pre-release
EXTRA=--prerelease
fi
github-release release \
--tag $TAG \
--name "$TAG release" $EXTRA
echo "$GITHUB_CLI_AUTH_TOKEN" | gh auth login --with-token
echo "Uploading debian x64 package"
github-release upload \
--tag $TAG \
--name "$DEBIAN_X64_BUILD" \
--file "dist/$DEBIAN_X64_BUILD"
echo "Uploading linux x64 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"
gh release create "$TAG" \
--title "$TAG release" \
--notes "" \
$EXTRA \
"dist/$DEBIAN_X64_BUILD" \
"dist/$LINUX_X64_BUILD" \
"dist/$WINDOWS_X64_BUILD" \
"dist/$MAC_X64_BUILD" \
"dist/$SERVER_BUILD"
echo "Building docker image"

View File

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

View File

@ -121,14 +121,14 @@ ws.subscribeToMessages(async message => {
return;
}
if (message.type === 'task-error') {
if (message.type === 'taskError') {
toastService.closePersistent(message.taskId);
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));
}
else if (message.type === 'task-succeeded') {
else if (message.type === 'taskSucceeded') {
const toast = makeToast(message.taskId, "Export finished successfully.");
toast.closeAfter = 5000;

View File

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

View File

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

View File

@ -153,12 +153,12 @@ ws.subscribeToMessages(async message => {
return;
}
if (message.type === 'task-error') {
if (message.type === 'taskError') {
toastService.closePersistent(message.taskId);
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));
} else if (message.type === 'task-succeeded') {
} else if (message.type === 'taskSucceeded') {
const toast = makeToast(message.taskId, "Delete finished successfully.");
toast.closeAfter = 5000;
@ -167,16 +167,16 @@ ws.subscribeToMessages(async message => {
});
ws.subscribeToMessages(async message => {
if (message.taskType !== 'undelete-notes') {
if (message.taskType !== 'undeleteNotes') {
return;
}
if (message.type === 'task-error') {
if (message.type === 'taskError') {
toastService.closePersistent(message.taskId);
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));
} else if (message.type === 'task-succeeded') {
} else if (message.type === 'taskSucceeded') {
const toast = makeToast(message.taskId, "Undeleting notes finished successfully.");
toast.closeAfter = 5000;

View File

@ -64,13 +64,12 @@ async function getWidgetBundlesByParent() {
try {
widget = await executeBundle(bundle);
widgetsByParent.add(widget);
}
catch (e) {
logError("Widget initialization failed: ", e);
continue;
}
widgetsByParent.add(widget);
}
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;
}
if (message.type === 'task-error') {
if (message.type === 'taskError') {
toastService.closePersistent(message.taskId);
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));
} else if (message.type === 'task-succeeded') {
} else if (message.type === 'taskSucceeded') {
const toast = makeToast(message.taskId, "Import finished successfully.");
toast.closeAfter = 5000;

View File

@ -89,7 +89,11 @@ function updateDisplayedShortcuts($container) {
const action = await getAction(actionName, true);
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%")
);
}
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 $openButton = $('<button class="file-open btn btn-primary" type="button">Open</button>');
$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
$openButton.toggle(!note.isProtected);
@ -59,7 +59,7 @@ async function getRenderedContent(note, options = {}) {
}
else if (type === '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)
.css("width", "100%");
@ -67,7 +67,7 @@ async function getRenderedContent(note, options = {}) {
}
else if (type === '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)
.css("width", "100%");

View File

@ -317,7 +317,9 @@ class NoteListRenderer {
const $expander = $('<span class="note-expander bx bx-chevron-right"></span>');
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">')
.attr('data-note-id', note.noteId)

View File

@ -21,9 +21,9 @@ function downloadFileNote(noteId) {
download(url);
}
async function openFileNote(noteId) {
async function openNoteExternally(noteId) {
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 res = await electron.shell.openPath(resp.tmpFilePath);
@ -66,7 +66,7 @@ function getHost() {
export default {
download,
downloadFileNote,
openFileNote,
openNoteExternally,
downloadNoteRevision,
getUrlForDownload
}

View File

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

View File

@ -2,8 +2,9 @@ import Component from "../widgets/component.js";
import appContext from "./app_context.js";
import dateNoteService from "../services/date_notes.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() {
import("../dialogs/jump_to_note.js").then(d => d.showDialog());
}
@ -84,4 +85,12 @@ export default class DialogCommandExecutor extends Component {
showBackendLogCommand() {
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 => {
if (message.type === 'open-note') {
if (message.type === 'openNote') {
appContext.tabManager.activateOrOpenNote(message.noteId);
if (utils.isElectron()) {

View File

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

View File

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

View File

@ -1,11 +1,12 @@
import TabAwareWidget from "./tab_aware_widget.js";
import protectedSessionService from "../services/protected_session.js";
import utils from "../services/utils.js";
const TPL = `
<div class="dropdown note-actions">
<style>
.note-actions .dropdown-menu {
width: 15em;
width: 20em;
}
.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="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="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 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>
@ -119,6 +121,8 @@ export default class NoteActionsWidget extends TabAwareWidget {
this.$widget.on('click', '.dropdown-item',
() => this.$widget.find('.dropdown-toggle').dropdown('toggle'));
this.$openNoteExternallyButton = this.$widget.find(".open-note-externally-button");
}
refreshWithNote(note) {
@ -128,6 +132,8 @@ export default class NoteActionsWidget extends TabAwareWidget {
this.$protectButton.toggle(!note.isProtected);
this.$unprotectButton.toggle(!!note.isProtected);
this.$openNoteExternallyButton.toggle(utils.isElectron());
}
toggleDisabled($el, enable) {

View File

@ -382,8 +382,6 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
else {
node.setActive();
this.clearSelectedNodes();
}
return false;
@ -393,6 +391,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
// click event won't propagate so let's close context menu manually
contextMenu.hide();
this.clearSelectedNodes();
const notePath = treeService.getNotePath(data.node);
const activeTabContext = appContext.tabManager.getActiveTabContext();
@ -1144,11 +1144,12 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
if (node) {
node.setActive(true, {noEvents: true, noFocus: !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 {
// 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">
<span class="bx bx-trash"></span>
Delete matched note
Delete matched notes
</td>
<td class="button-column">
<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="ownedRelationCount">Number of relations</option>
<option value="targetRelationCount">Number of relations targeting the note</option>
<option value="random">Random order</option>
</select>
<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.$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.$uploadNewRevisionInput.trigger("click");

View File

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

View File

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

View File

@ -107,7 +107,11 @@ function processContent(images, note, content) {
const filename = path.basename(src);
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;
}
@ -140,7 +144,7 @@ function processContent(images, note, content) {
function openNote(req) {
if (utils.isElectron()) {
ws.sendMessageToAllClients({
type: 'open-note',
type: 'openNote',
noteId: req.params.noteId
});

View File

@ -3,10 +3,13 @@
const protectedSessionService = require('../../services/protected_session');
const repository = require('../../services/repository');
const utils = require('../../services/utils');
const log = require('../../services/log');
const noteRevisionService = require('../../services/note_revisions');
const tmp = require('tmp');
const fs = require('fs');
const { Readable } = require('stream');
const chokidar = require('chokidar');
const ws = require('../../services/ws');
function updateFile(req) {
const {noteId} = req.params;
@ -120,6 +123,19 @@ function saveToTmpDir(req) {
fs.writeSync(tmpObj.fd, note.getContent());
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 {
tmpFilePath: tmpObj.name
};

View File

@ -7,6 +7,8 @@ const sql = require('../../services/sql');
const utils = require('../../services/utils');
const log = require('../../services/log');
const TaskContext = require('../../services/task_context');
const fs = require('fs');
const noteRevisionService = require("../../services/note_revisions.js");
function getNote(req) {
const noteId = req.params.noteId;
@ -80,7 +82,7 @@ function deleteNote(req) {
function undeleteNote(req) {
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);
@ -109,7 +111,7 @@ function protectNote(req) {
const protect = !!parseInt(req.params.isProtected);
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);
@ -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 = {
getNote,
updateNote,
@ -286,5 +311,6 @@ module.exports = {
changeTitle,
duplicateSubtree,
eraseDeletedNotesNow,
getDeleteNotesPreview
getDeleteNotesPreview,
uploadModifiedFile
};

View File

@ -3,6 +3,7 @@
const utils = require('../services/utils');
const optionService = require('../services/options');
const myScryptService = require('../services/my_scrypt');
const log = require('../services/log');
function loginPage(req, res) {
res.render('login', { failedAuth: false });
@ -28,6 +29,9 @@ function login(req, res) {
});
}
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});
}
}

View File

@ -165,6 +165,7 @@ function register(app) {
apiRoute(POST, '/api/notes/erase-deleted-notes-now', notesApiRoute.eraseDeletedNotesNow);
apiRoute(PUT, '/api/notes/:noteId/change-title', notesApiRoute.changeTitle);
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);
@ -177,14 +178,15 @@ function register(app) {
route(PUT, '/api/notes/:noteId/file', [auth.checkApiAuthOrElectron, uploadMiddleware, csrfMiddleware],
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, {
debug: (string, extra) => { console.log(string, extra); }
}));
route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile);
// this "hacky" path is used for easier referencing of CSS resources
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(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) {
title = outline.$.title;
content = toHtml(outline.$.text);
if (!title || !title.trim()) {
// https://github.com/zadam/trilium/issues/1862
title = outline.$.text;
content = '';
}
}
else if (opmlVersion === 2) {
title = outline.$.text;

View File

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

View File

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

View File

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

View File

@ -28,17 +28,33 @@ class OrderByAndLimitExp extends Expression {
let valA = valueExtractor.extract(a);
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);
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;
} else if (valA > valB) {
} else if (!valA || valA > valB) {
return larger;
}
// else go to next order definition
// else the values are equal and continue to next order definition
}
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') {
return OrExp.of(expressions);
}
else {
throw new Error(`Unrecognized op=${op}`);
}
}
for (i = 0; i < tokens.length; i++) {
@ -358,8 +361,7 @@ function getExpression(tokens, searchContext, level = 0) {
continue;
}
exp.subExpression = getAggregateExpression();
exp.subExpression = getAggregateExpression();console.log(exp);
return exp;
}
else if (token === 'not') {

View File

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

View File

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

View File

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

View File

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

View File

@ -18,12 +18,12 @@
<ul>
<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 data-command="backInNoteHistory"></kbd>, <kbd data-command="forwardInNoteHistory"></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="scrollToActiveNote"></kbd> - scroll to active note</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">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">not set</kbd> - scroll to active 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="collapseSubtree"></kbd> - collapse sub-tree</li>
<li><kbd data-command="collapseTree">not set</kbd> - collapse whole note tree</li>
<li><kbd data-command="collapseSubtree">not set</kbd> - collapse sub-tree</li>
</ul>
</p>
</div>
@ -40,10 +40,10 @@
Only in desktop (electron build):
<ul>
<li><kbd data-command="openNewTab"></kbd> open empty tab</li>
<li><kbd data-command="closeActiveTab"></kbd> close active tab</li>
<li><kbd data-command="activateNextTab"></kbd> activate next tab</li>
<li><kbd data-command="activatePreviousTab"></kbd> activate previous tab</li>
<li><kbd data-command="openNewTab">not set</kbd> open empty tab</li>
<li><kbd data-command="closeActiveTab">not set</kbd> close active tab</li>
<li><kbd data-command="activateNextTab">not set</kbd> activate next tab</li>
<li><kbd data-command="activatePreviousTab">not set</kbd> activate previous tab</li>
</ul>
</p>
</div>
@ -55,9 +55,9 @@
<p class="card-text">
<ul>
<li><kbd data-command="createNoteAfter"></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="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="createNoteAfter">not set</kbd> - create new note after the active note</li>
<li><kbd data-command="createNoteInto">not set</kbd> - create new sub-note into active note</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>
</p>
</div>
@ -69,15 +69,15 @@
<p class="card-text">
<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="moveNoteUpInHierarchy"></kbd>, <kbd data-command="moveNoteDownInHierarchy"></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="selectAllNotesInParent"></kbd> - select all notes in the current level</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">not set</kbd>, <kbd data-command="moveNoteDownInHierarchy">not set</kbd> - move note up in the hierarchy</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">not set</kbd> - select all notes in the current level</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="cutNotesToClipboard"></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="deleteNotes"></kbd> - delete note / sub-tree</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">not set</kbd> - cut current (or current selection) note into clipboard (used for moving notes)</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">not set</kbd> - delete note / sub-tree</li>
</ul>
</p>
</div>
@ -89,12 +89,12 @@
<p class="card-text">
<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.
<kbd data-command="scrollToActiveNote"></kbd> will switch back from editor to tree pane.</li>
<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">not set</kbd> will switch back from editor to tree pane.</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="insertDateTimeToText"></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="addLinkToText">not set</kbd> - create internal link</li>
<li><kbd data-command="insertDateTimeToText">not set</kbd> - insert current date and time at caret position</li>
<li><kbd data-command="scrollToActiveNote">not set</kbd> - jump away to the tree pane and scroll to active note</li>
</ul>
</p>
</div>
@ -121,9 +121,9 @@
<p class="card-text">
<ul>
<li><kbd data-command="reloadFrontendApp"></kbd> - reload Trilium frontend</li>
<li><kbd data-command="openDevTools"></kbd> - show developer tools</li>
<li><kbd data-command="showSQLConsole"></kbd> - show SQL console</li>
<li><kbd data-command="reloadFrontendApp">not set</kbd> - reload Trilium frontend</li>
<li><kbd data-command="openDevTools">not set</kbd> - show developer tools</li>
<li><kbd data-command="showSQLConsole">not set</kbd> - show SQL console</li>
</ul>
</p>
</div>
@ -135,9 +135,9 @@
<p class="card-text">
<ul>
<li><kbd data-command="toggleZenMode"></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="findInText"></kbd> - in page search</li>
<li><kbd data-command="toggleZenMode">not set</kbd> - Zen mode - display only note editor, everything else is hidden</li>
<li><kbd data-command="quickSearch">not set</kbd> - focus on quick search input</li>
<li><kbd data-command="findInText">not set</kbd> - in page search</li>
</ul>
</p>
</div>

11
src/www
View File

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

View File

@ -10,6 +10,9 @@
<excludeFolder url="file://$MODULE_DIR$/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$/docs" />
<excludeFolder url="file://$MODULE_DIR$/bin/better-sqlite3" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />