Merge branch 'zadam:master' into master

This commit is contained in:
Teven Feng 2023-05-19 07:56:17 +08:00 committed by GitHub
commit 1c02380026
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 2923 additions and 176 deletions

2809
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"name": "trilium", "name": "trilium",
"productName": "Trilium Notes", "productName": "Trilium Notes",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.59.4", "version": "0.60.0-beta",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {
@ -45,13 +45,13 @@
"cookie-parser": "1.4.6", "cookie-parser": "1.4.6",
"csurf": "1.11.0", "csurf": "1.11.0",
"dayjs": "1.11.7", "dayjs": "1.11.7",
"dayjs-plugin-utc": "^0.1.2", "dayjs-plugin-utc": "0.1.2",
"debounce": "^1.2.1", "debounce": "1.2.1",
"ejs": "3.1.9", "ejs": "3.1.9",
"electron-debug": "3.2.0", "electron-debug": "3.2.0",
"electron-dl": "3.5.0", "electron-dl": "3.5.0",
"electron-window-state": "5.0.3", "electron-window-state": "5.0.3",
"escape-html": "^1.0.3", "escape-html": "1.0.3",
"express": "4.18.2", "express": "4.18.2",
"express-partial-content": "1.0.2", "express-partial-content": "1.0.2",
"express-rate-limit": "6.7.0", "express-rate-limit": "6.7.0",
@ -101,21 +101,21 @@
"electron-builder": "23.6.0", "electron-builder": "23.6.0",
"electron-packager": "17.1.1", "electron-packager": "17.1.1",
"electron-rebuild": "3.2.9", "electron-rebuild": "3.2.9",
"eslint": "^8.38.0", "eslint": "8.38.0",
"eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "8.8.0",
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "2.27.5",
"eslint-plugin-jsonc": "^2.7.0", "eslint-plugin-jsonc": "2.7.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "4.2.1",
"esm": "3.2.25", "esm": "3.2.25",
"husky": "^8.0.3", "husky": "8.0.3",
"jsonc-eslint-parser": "^2.2.0", "jsonc-eslint-parser": "2.2.0",
"lint-staged": "^13.2.1", "lint-staged": "13.2.1",
"jasmine": "4.6.0", "jasmine": "4.6.0",
"jsdoc": "4.0.2", "jsdoc": "4.0.2",
"lorem-ipsum": "2.0.8", "lorem-ipsum": "2.0.8",
"prettier": "2.8.7", "prettier": "2.8.7",
"nodemon": "^2.0.22", "nodemon": "2.0.22",
"rcedit": "3.0.1", "rcedit": "3.0.1",
"webpack": "5.78.0", "webpack": "5.78.0",
"webpack-cli": "5.0.1" "webpack-cli": "5.0.1"

View File

@ -226,6 +226,10 @@ class NoteContext extends Component {
return true; return true;
} }
if (this.viewScope.viewMode === 'source') {
return true;
}
const noteComplement = await this.getNoteComplement(); const noteComplement = await this.getNoteComplement();
const sizeLimit = this.note.type === 'text' const sizeLimit = this.note.type === 'text'

View File

@ -55,6 +55,13 @@ export default class RootCommandExecutor extends Component {
} }
} }
openNoteCustomCommand() {
const noteId = appContext.tabManager.getActiveContextNoteId();
if (noteId) {
openService.openNoteCustom(noteId);
}
}
enterProtectedSessionCommand() { enterProtectedSessionCommand() {
protectedSessionService.enterProtectedSession(); protectedSessionService.enterProtectedSession();
} }

View File

@ -1 +1 @@
<p>Keyboard launcher for this launcher action can be configured in Options -> Launchers.</p> <p>Keyboard shortcut for this launcher action can be configured in Options -> Shortcuts.</p>

View File

@ -3,7 +3,7 @@
<ol> <ol>
<li><code>target</code> - note which should be opened upon activating the launcher</li> <li><code>target</code> - note which should be opened upon activating the launcher</li>
<li><code>hoistedNote</code> - optional, will change the hoisted note before opening the target note</li> <li><code>hoistedNote</code> - optional, will change the hoisted note before opening the target note</li>
<li><code>keyboardLauncher</code> - optional, pressing the keyboard launcher will open the note</li> <li><code>keyboardShortcut</code> - optional, pressing the keyboard shortcut will open the note</li>
</ol> </ol>
<p>Launchbar displays the title / icon from the launcher which does not necessarily mirrors those of the target note.</p> <p>Launchbar displays the title / icon from the launcher which does not necessarily mirror those of the target note.</p>

View File

@ -2,7 +2,7 @@
<ol> <ol>
<li><code>script</code> - relation to the script note which should be executed upon launcher activation</li> <li><code>script</code> - relation to the script note which should be executed upon launcher activation</li>
<li><code>keyboardLauncher</code> - optional, pressing the keyboard launcher will activate the launcher</li> <li><code>keyboardShortcut</code> - optional, pressing the keyboard shortcut will activate the launcher</li>
</ol> </ol>
<h4>Example script</h4> <h4>Example script</h4>

View File

@ -47,6 +47,67 @@ async function openNoteExternally(noteId, mime) {
} }
} }
async function openNoteCustom(noteId) {
if (!utils.isElectron() || utils.isMac()) {
return;
}
const resp = await server.post(`notes/${noteId}/save-to-tmp-dir`);
let filePath = resp.tmpFilePath;
const {exec} = utils.dynamicRequire('child_process');
const platform = process.platform;
if (platform === 'linux') {
// we don't know which terminal is available, try in succession
const terminals = ['x-terminal-emulator', 'gnome-terminal', 'konsole', 'xterm', 'xfce4-terminal', 'mate-terminal', 'rxvt', 'terminator', 'terminology'];
const openFileWithTerminal = (terminal) => {
const command = `${terminal} -e 'mimeopen -d "${filePath}"'`;
console.log(`Open Note custom: ${command} `);
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`Open Note custom: Failed to open file with ${terminal}: ${error}`);
searchTerminal(terminals.indexOf(terminal) + 1);
} else {
console.log(`Open Note custom: File opened with ${terminal}: ${stdout}`);
}
});
};
const searchTerminal = (index) => {
const terminal = terminals[index];
if (!terminal) {
console.error('Open Note custom: No terminal found!');
open(getFileUrl(noteId), {url: true});
return;
}
exec(`which ${terminal}`, (error, stdout, stderr) => {
if (stdout.trim()) {
openFileWithTerminal(terminal);
} else {
searchTerminal(index + 1);
}
});
};
searchTerminal(0);
} else if (platform === 'win32') {
if (filePath.indexOf("/") !== -1) {
// Note that the path separator must be \ instead of /
filePath = filePath.replace(/\//g, "\\");
}
const command = `rundll32.exe shell32.dll,OpenAs_RunDLL ` + filePath;
exec(command, (err, stdout, stderr) => {
if (err) {
console.error("Open Note custom: ", err);
open(getFileUrl(noteId), {url: true});
return;
}
});
} else {
console.log('Currently "Open Note custom" only supports linux and windows systems');
open(getFileUrl(noteId), {url: true});
}
}
function downloadNoteRevision(noteId, noteRevisionId) { function downloadNoteRevision(noteId, noteRevisionId) {
const url = getUrlForDownload(`api/notes/${noteId}/revisions/${noteRevisionId}/download`); const url = getUrlForDownload(`api/notes/${noteId}/revisions/${noteRevisionId}/download`);
@ -76,6 +137,7 @@ export default {
download, download,
downloadFileNote, downloadFileNote,
openNoteExternally, openNoteExternally,
openNoteCustom,
downloadNoteRevision, downloadNoteRevision,
getUrlForDownload getUrlForDownload
} }

View File

@ -5,7 +5,9 @@ import protectedSessionHolder from "../../services/protected_session_holder.js";
export default class EditButton extends OnClickButtonWidget { export default class EditButton extends OnClickButtonWidget {
isEnabled() { isEnabled() {
return super.isEnabled() && this.note; return super.isEnabled()
&& this.note
&& this.noteContext.viewScope.viewMode === 'default';
} }
constructor() { constructor() {

View File

@ -29,6 +29,7 @@ const TPL = `
<a data-trigger-command="findInText" class="dropdown-item find-in-text-button">Search in note <kbd data-command="findInText"></a> <a data-trigger-command="findInText" class="dropdown-item find-in-text-button">Search in note <kbd data-command="findInText"></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 data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"><kbd data-command="openNoteExternally"></kbd> Open note externally</a>
<a data-trigger-command="openNoteCustom" class="dropdown-item open-note-custom-button"><kbd data-command="openNoteCustom"></kbd> Open note custom</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 class="dropdown-item delete-note-button">Delete note</a> <a class="dropdown-item delete-note-button">Delete note</a>
@ -67,6 +68,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
this.$widget.on('click', '.dropdown-item', () => this.$widget.find("[data-toggle='dropdown']").dropdown('toggle')); this.$widget.on('click', '.dropdown-item', () => this.$widget.find("[data-toggle='dropdown']").dropdown('toggle'));
this.$openNoteExternallyButton = this.$widget.find(".open-note-externally-button"); this.$openNoteExternallyButton = this.$widget.find(".open-note-externally-button");
this.$openNoteCustomButton = this.$widget.find(".open-note-custom-button");
this.$deleteNoteButton = this.$widget.find(".delete-note-button"); this.$deleteNoteButton = this.$widget.find(".delete-note-button");
this.$deleteNoteButton.on("click", () => { this.$deleteNoteButton.on("click", () => {
@ -88,6 +90,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
this.$renderNoteButton.toggle(note.type === 'render'); this.$renderNoteButton.toggle(note.type === 'render');
this.$openNoteExternallyButton.toggle(utils.isElectron()); this.$openNoteExternallyButton.toggle(utils.isElectron());
this.$openNoteCustomButton.toggle(utils.isElectron() && !utils.isMac()); // no implementation for Mac yet
} }
toggleDisabled($el, enable) { toggleDisabled($el, enable) {

View File

@ -6,6 +6,7 @@ export default class ScrollingContainer extends Container {
this.class("scrolling-container"); this.class("scrolling-container");
this.css('overflow', 'auto'); this.css('overflow', 'auto');
this.css('scroll-behavior', 'smooth');
this.css('position', 'relative'); this.css('position', 'relative');
} }

View File

@ -146,20 +146,31 @@ export default class FindWidget extends NoteContextAwareWidget {
return; return;
} }
this.handler = await this.getHandler();
const selectedText = window.getSelection().toString() || "";
this.$findBox.show(); this.$findBox.show();
this.$input.focus(); this.$input.focus();
this.handler = await this.getHandler();
const isAlreadyVisible = this.$findBox.is(":visible"); const isAlreadyVisible = this.$findBox.is(":visible");
if (isAlreadyVisible) { if (isAlreadyVisible) {
if (selectedText) {
this.$input.val(selectedText);
}
if (this.$input.val()) {
await this.performFind();
}
this.$input.select(); this.$input.select();
} else { } else {
this.$totalFound.text(0); this.$totalFound.text(0);
this.$currentFound.text(0); this.$currentFound.text(0);
const searchTerm = await this.handler.getInitialSearchTerm(); this.$input.val(selectedText);
this.$input.val(searchTerm || "");
if (searchTerm !== "") { if (selectedText) {
this.$input.select(); this.$input.select();
await this.performFind(); await this.performFind();
} }

View File

@ -16,23 +16,6 @@ export default class FindInCode {
return this.parent.noteContext.getCodeEditor(); return this.parent.noteContext.getCodeEditor();
} }
async getInitialSearchTerm() {
const codeEditor = await this.getCodeEditor();
// highlightSelectionMatches is the overlay that highlights
// the words under the cursor. This occludes the search
// markers style, save it, disable it. Will be restored when
// the focus is back into the note
this.oldHighlightSelectionMatches = codeEditor.getOption("highlightSelectionMatches");
codeEditor.setOption("highlightSelectionMatches", false);
// Fill in the findbox with the current selection if any
const selectedText = codeEditor.getSelection()
if (selectedText !== "") {
return selectedText;
}
}
async performFind(searchTerm, matchCase, wholeWord) { async performFind(searchTerm, matchCase, wholeWord) {
let findResult = null; let findResult = null;
let totalFound = 0; let totalFound = 0;

View File

@ -16,10 +16,6 @@ export default class FindInHtml {
this.$results = null; this.$results = null;
} }
async getInitialSearchTerm() {
return ""; // FIXME
}
async performFind(searchTerm, matchCase, wholeWord) { async performFind(searchTerm, matchCase, wholeWord) {
await libraryLoader.requireLibrary(libraryLoader.MARKJS); await libraryLoader.requireLibrary(libraryLoader.MARKJS);

View File

@ -8,19 +8,6 @@ export default class FindInText {
return this.parent.noteContext.getTextEditor(); return this.parent.noteContext.getTextEditor();
} }
async getInitialSearchTerm() {
const textEditor = await this.getTextEditor();
const selection = textEditor.model.document.selection;
const range = selection.getFirstRange();
// FIXME
for (const item of range.getItems()) {
// Fill in the findbox with the current selection if any
return item.data;
}
}
async performFind(searchTerm, matchCase, wholeWord) { async performFind(searchTerm, matchCase, wholeWord) {
// Do this even if the searchTerm is empty so the markers are cleared and // Do this even if the searchTerm is empty so the markers are cleared and
// the counters updated // the counters updated

View File

@ -69,7 +69,8 @@ export default class TocWidget extends RightPanelWidget {
isEnabled() { isEnabled() {
return super.isEnabled() return super.isEnabled()
&& this.note.type === 'text' && this.note.type === 'text'
&& !this.noteContext.viewScope.tocTemporarilyHidden; && !this.noteContext.viewScope.tocTemporarilyHidden
&& this.noteContext.viewScope.viewMode === 'default';
} }
async doRenderBody() { async doRenderBody() {
@ -176,7 +177,7 @@ export default class TocWidget extends RightPanelWidget {
const headingElement = $container.find(":header")[headingIndex]; const headingElement = $container.find(":header")[headingIndex];
if (headingElement != null) { if (headingElement != null) {
headingElement.scrollIntoView(); headingElement.scrollIntoView({ behavior: "smooth" });
} }
} else { } else {
const textEditor = await this.noteContext.getTextEditor(); const textEditor = await this.noteContext.getTextEditor();
@ -192,50 +193,9 @@ export default class TocWidget extends RightPanelWidget {
// navigate (note that the TOC rendering and other TOC // navigate (note that the TOC rendering and other TOC
// entries' navigation could be wrong too) // entries' navigation could be wrong too)
if (headingNode != null) { if (headingNode != null) {
// Setting the selection alone doesn't scroll to the $(textEditor.editing.view.domRoots.values().next().value).find(':header')[headingIndex].scrollIntoView({
// caret, needs to be done explicitly and outside of behavior: 'smooth'
// the writer change callback so the scroll is
// guaranteed to happen after the selection is
// updated.
// In addition, scrolling to a caret later in the
// document (ie "forward scrolls"), only scrolls
// barely enough to place the caret at the bottom of
// the screen, which is a usability issue, you would
// like the caret to be placed at the top or center
// of the screen.
// To work around that issue, first scroll to the
// end of the document, then scroll to the desired
// point. This causes all the scrolls to be
// "backward scrolls" no matter the current caret
// position, which places the caret at the top of
// the screen.
// XXX This could be fixed in another way by using
// the underlying CKEditor5
// scrollViewportToShowTarget, which allows to
// provide a larger "viewportOffset", but that
// has coding complications (requires calling an
// internal CKEditor utils funcion and passing
// an HTML element, not a CKEditor node, and
// CKEditor5 doesn't seem to have a
// straightforward way to convert a node to an
// HTML element? (in CKEditor4 this was done
// with $(node.$) )
// Scroll to the end of the note to guarantee the
// next scroll is a backwards scroll that places the
// caret at the top of the screen
model.change(writer => {
writer.setSelection(root.getChild(root.childCount - 1), 0);
}); });
textEditor.editing.view.scrollToTheSelection();
// Backwards scroll to the heading
model.change(writer => {
writer.setSelection(headingNode, 0);
});
textEditor.editing.view.scrollToTheSelection();
} }
} }
} }
@ -302,4 +262,4 @@ class CloseTocButton extends OnClickButtonWidget {
}) })
.class("icon-action close-toc"); .class("icon-action close-toc");
} }
} }

View File

@ -12,6 +12,14 @@ const TPL = `
.excalidraw .App-menu_top .buttonList { .excalidraw .App-menu_top .buttonList {
display: flex; display: flex;
} }
/* Conflict between excalidraw and bootstrap classes keeps the menu hidden */
/* https://github.com/zadam/trilium/issues/3780 */
/* https://github.com/excalidraw/excalidraw/issues/6567 */
.excalidraw .dropdown-menu {
display: block;
}
.excalidraw-wrapper { .excalidraw-wrapper {
height: 100%; height: 100%;

View File

@ -51,7 +51,7 @@ const TPL = `
cursor: text !important; cursor: text !important;
} }
.note-detail-editable-text *:not(figure):first-child { .note-detail-editable-text *:not(figure,.include-note):first-child {
margin-top: 0 !important; margin-top: 0 !important;
} }

View File

@ -48,7 +48,7 @@ export default class ImageOptions extends OptionsWidget {
this.updateOption('imageMaxWidthHeight', this.$imageMaxWidthHeight.val())); this.updateOption('imageMaxWidthHeight', this.$imageMaxWidthHeight.val()));
this.$imageJpegQuality.on('change', () => this.$imageJpegQuality.on('change', () =>
this.updateOption('imageJpegQuality', this.$imageJpegQuality.val())); this.updateOption('imageJpegQuality', this.$imageJpegQuality.val().trim() || "75"));
this.$downloadImagesAutomatically = this.$widget.find(".download-images-automatically"); this.$downloadImagesAutomatically = this.$widget.find(".download-images-automatically");

Binary file not shown.

View File

@ -6,7 +6,7 @@
@font-face { @font-face {
font-family: Montserrat; font-family: Montserrat;
src: url(../fonts/Montserrat-Regular.ttf); src: url(../fonts/Montserrat-SemiBold.ttf);
font-weight: bold; font-weight: bold;
} }
@ -392,6 +392,42 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
.bs-tooltip-left .arrow::before { border-left-color: var(--main-border-color) !important; } .bs-tooltip-left .arrow::before { border-left-color: var(--main-border-color) !important; }
.bs-tooltip-right .arrow::before { border-right-color: var(--main-border-color) !important; } .bs-tooltip-right .arrow::before { border-right-color: var(--main-border-color) !important; }
.bs-tooltip-bottom .arrow::after { border-bottom-color: var(--tooltip-background-color) !important; }
.bs-tooltip-top .arrow::after { border-top-color: var(--tooltip-background-color) !important; }
.bs-tooltip-left .arrow::after { border-left-color: var(--tooltip-background-color) !important; }
.bs-tooltip-right .arrow::after { border-right-color: var(--tooltip-background-color) !important; }
.tooltip .arrow::after {
position: absolute;
content: '';
border-color: transparent;
border-style: solid;
}
.bs-tooltip-auto[x-placement^='left'] .arrow::after,
.bs-tooltip-left .arrow::after {
left: -1px;
border-width: 0.4rem 0 0.4rem 0.4rem;
}
.bs-tooltip-auto[x-placement^='bottom'] .arrow::after,
.bs-tooltip-bottom .arrow::after {
bottom: -1px;
border-width: 0 0.4rem 0.4rem;
}
.bs-tooltip-auto[x-placement^='right'] .arrow::after,
.bs-tooltip-right .arrow::after {
right: -1px;
border-width: 0.4rem 0.4rem 0.4rem 0;
}
.bs-tooltip-auto[x-placement^='top'] .arrow::after,
.bs-tooltip-top .arrow::after {
top: -1px;
border-width: 0.4rem 0.4rem 0;
}
.note-tooltip.tooltip .arrow { .note-tooltip.tooltip .arrow {
display: none; display: none;
} }

View File

@ -110,8 +110,8 @@ function checkCredentials(req, res, next) {
const header = req.headers['trilium-cred'] || ''; const header = req.headers['trilium-cred'] || '';
const auth = new Buffer.from(header, 'base64').toString(); const auth = new Buffer.from(header, 'base64').toString();
const [username, password] = auth.split(/:/); const colonIndex = auth.indexOf(':');
const password = colonIndex === -1 ? "" : auth.substr(colonIndex + 1);
// username is ignored // username is ignored
if (!passwordEncryptionService.verifyPassword(password)) { if (!passwordEncryptionService.verifyPassword(password)) {

View File

@ -1 +1 @@
module.exports = { buildDate:"2023-04-17T21:40:35+02:00", buildRevision: "1d3272e9f8c27106a66227fbb580677ae5d70427" }; module.exports = { buildDate:"2023-05-18T23:31:57+02:00", buildRevision: "14dd2b882750ea5484d1aba1f2b57c931bc76e9c" };

View File

@ -134,7 +134,7 @@ function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSwitch,
} }
async function shrinkImage(buffer, originalName) { async function shrinkImage(buffer, originalName) {
let jpegQuality = optionService.getOptionInt('imageJpegQuality'); let jpegQuality = optionService.getOptionInt('imageJpegQuality', 0);
if (jpegQuality < 10 || jpegQuality > 100) { if (jpegQuality < 10 || jpegQuality > 100) {
jpegQuality = 75; jpegQuality = 75;

View File

@ -10,7 +10,7 @@ function getOptionOrNull(name) {
// e.g. in initial sync becca is not loaded because DB is not initialized // e.g. in initial sync becca is not loaded because DB is not initialized
option = sql.getRow("SELECT * FROM options WHERE name = ?", name); option = sql.getRow("SELECT * FROM options WHERE name = ?", name);
} }
return option ? option.value : null; return option ? option.value : null;
} }
@ -27,13 +27,17 @@ function getOption(name) {
/** /**
* @returns {number} * @returns {number}
*/ */
function getOptionInt(name) { function getOptionInt(name, defaultValue = undefined) {
const val = getOption(name); const val = getOption(name);
const intVal = parseInt(val); const intVal = parseInt(val);
if (isNaN(intVal)) { if (isNaN(intVal)) {
throw new Error(`Could not parse "${val}" into integer for option "${name}"`); if (defaultValue === undefined) {
throw new Error(`Could not parse "${val}" into integer for option "${name}"`);
} else {
return defaultValue;
}
} }
return intVal; return intVal;