Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46fdd15857 | |||
| 42cd333694 | |||
| aaa69f696b | |||
| 8ce9dcf4aa | |||
| 130ded91ff | |||
| 851465da62 | |||
| 0480f391d3 | |||
| 36c98e919a | |||
| dacc2444c4 | |||
| 8ea3608bf1 | |||
| a402c79287 | |||
| ee667634ab | |||
| 7a8c69a6f9 | |||
| a76bef1a28 | |||
| a259bd8c3a | |||
| 4001953fd7 | |||
| 9a6df54832 | |||
| 29327bbc5d | |||
| bcb3a707f4 | |||
| 0a67af4f46 | |||
| b3c0b36ba6 | |||
| a3783b0113 | |||
| 059dca905c | |||
| 02d908df1e | |||
| 1a95e459eb | |||
| ae0c5a0c09 | |||
| 6f238f5f1a | |||
| 6cec8bcda8 | |||
| d388b4d814 | |||
| d9dac00a01 | |||
| 4961d9bb89 | |||
| 89e319c7a2 |
+12
-5
@@ -4,6 +4,9 @@ FROM node:16.18.0-alpine
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Bundle app source
|
||||
COPY . .
|
||||
|
||||
COPY server-package.json package.json
|
||||
|
||||
# Install app dependencies
|
||||
@@ -18,15 +21,19 @@ RUN set -x \
|
||||
nasm \
|
||||
libpng-dev \
|
||||
python3 \
|
||||
&& npm install --production \
|
||||
&& apk del .build-dependencies
|
||||
&& npm install \
|
||||
&& apk del .build-dependencies \
|
||||
&& npm run webpack \
|
||||
&& npm prune --omit=dev \
|
||||
# Set the path to the newly created webpack bundle
|
||||
&& sed -i -e 's/app\/desktop.js/app-dist\/desktop.js/g' src/views/desktop.ejs \
|
||||
&& sed -i -e 's/app\/mobile.js/app-dist\/mobile.js/g' src/views/mobile.ejs \
|
||||
&& sed -i -e 's/app\/setup.js/app-dist\/setup.js/g' src/views/setup.ejs \
|
||||
&& sed -i -e 's/app\/share.js/app-dist\/share.js/g' src/views/share/*.ejs
|
||||
|
||||
# Some setup tools need to be kept
|
||||
RUN apk add --no-cache su-exec shadow
|
||||
|
||||
# Bundle app source
|
||||
COPY . .
|
||||
|
||||
# Add application user and setup proper volume permissions
|
||||
RUN adduser -s /bin/false node; exit 0
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ app.on('ready', async () => {
|
||||
|
||||
// if db is not initialized -> setup process
|
||||
// if db is initialized, then we need to wait until the migration process is finished
|
||||
if (await sqlInit.isDbInitialized()) {
|
||||
if (sqlInit.isDbInitialized()) {
|
||||
await sqlInit.dbReady;
|
||||
|
||||
await windowService.createMainWindow(app);
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"version": "0.56.2",
|
||||
"version": "0.57.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "trilium",
|
||||
"version": "0.56.2",
|
||||
"version": "0.57.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.57.0-beta",
|
||||
"version": "0.57.3",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
|
||||
@@ -52,6 +52,8 @@ app.use(`/node_modules/@excalidraw/excalidraw/dist/`, express.static(path.join(_
|
||||
app.use(`/${assetPath}/node_modules/@excalidraw/excalidraw/dist/`, express.static(path.join(__dirname, '..', 'node_modules/@excalidraw/excalidraw/dist/')));
|
||||
app.use(`/${assetPath}/images`, express.static(path.join(__dirname, '..', 'images')));
|
||||
app.use(`/assets/vX/images`, express.static(path.join(__dirname, '..', 'images')));
|
||||
app.use(`/manifest.webmanifest`, express.static(path.join(__dirname, 'public/manifest.webmanifest')));
|
||||
app.use(`/robots.txt`, express.static(path.join(__dirname, 'public/robots.txt')));
|
||||
const sessionParser = session({
|
||||
secret: sessionSecret,
|
||||
resave: false, // true forces the session to be saved back to the session store, even if the session was never modified during the request.
|
||||
|
||||
@@ -81,6 +81,18 @@ import MermaidExportButton from "../widgets/floating_buttons/mermaid_export_butt
|
||||
import EditableCodeButtonsWidget from "../widgets/type_widgets/editable_code_buttons.js";
|
||||
import ApiLogWidget from "../widgets/api_log.js";
|
||||
import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js";
|
||||
import AppearanceOptions from "../widgets/dialogs/options/appearance.js";
|
||||
import KeyboardShortcutsOptions from "../widgets/dialogs/options/shortcuts.js";
|
||||
import TextNotesOptions from "../widgets/dialogs/options/text_notes.js";
|
||||
import CodeNotesOptions from "../widgets/dialogs/options/code_notes.js";
|
||||
import ImageOptions from "../widgets/dialogs/options/images.js";
|
||||
import SpellcheckOptions from "../widgets/dialogs/options/spellcheck.js";
|
||||
import PasswordOptions from "../widgets/dialogs/options/password.js";
|
||||
import EtapiOptions from "../widgets/dialogs/options/etapi.js";
|
||||
import BackupOptions from "../widgets/dialogs/options/backup.js";
|
||||
import SyncOptions from "../widgets/dialogs/options/sync.js";
|
||||
import OtherOptions from "../widgets/dialogs/options/other.js";
|
||||
import AdvancedOptions from "../widgets/dialogs/options/advanced.js";
|
||||
|
||||
export default class DesktopLayout {
|
||||
constructor(customWidgets) {
|
||||
@@ -241,6 +253,19 @@ export default class DesktopLayout {
|
||||
.child(new InfoDialog())
|
||||
.child(new ConfirmDialog())
|
||||
.child(new PromptDialog())
|
||||
.child(new OptionsDialog());
|
||||
.child(new OptionsDialog()
|
||||
.child(new AppearanceOptions())
|
||||
.child(new KeyboardShortcutsOptions())
|
||||
.child(new TextNotesOptions())
|
||||
.child(new CodeNotesOptions())
|
||||
.child(new ImageOptions())
|
||||
.child(new SpellcheckOptions())
|
||||
.child(new PasswordOptions())
|
||||
.child(new EtapiOptions())
|
||||
.child(new BackupOptions())
|
||||
.child(new SyncOptions())
|
||||
.child(new OtherOptions())
|
||||
.child(new AdvancedOptions())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ async function createNoteLink(notePath, options = {}) {
|
||||
return $("<span>").text("[missing note]");
|
||||
}
|
||||
|
||||
if (!notePath.startsWith("root")) {
|
||||
// all note paths should start with "root/" (except for "root" itself)
|
||||
// used e.g. to find internal links
|
||||
notePath = "root/" + notePath;
|
||||
}
|
||||
|
||||
let noteTitle = options.title;
|
||||
const showTooltip = options.showTooltip === undefined ? true : options.showTooltip;
|
||||
const showNotePath = options.showNotePath === undefined ? false : options.showNotePath;
|
||||
|
||||
@@ -230,7 +230,19 @@ function focusSavedElement() {
|
||||
return;
|
||||
}
|
||||
|
||||
$lastFocusedElement.focus();
|
||||
if ($lastFocusedElement.hasClass("ck")) {
|
||||
// must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607
|
||||
// the bug manifests itself in resetting the cursor position to the first character - jumping above
|
||||
|
||||
const editor = $lastFocusedElement
|
||||
.closest('.ck-editor__editable')
|
||||
.prop('ckeditorInstance');
|
||||
|
||||
editor.editing.view.focus();
|
||||
} else {
|
||||
$lastFocusedElement.focus();
|
||||
}
|
||||
|
||||
$lastFocusedElement = null;
|
||||
}
|
||||
|
||||
@@ -241,7 +253,7 @@ async function openDialog($dialog, closeActDialog = true) {
|
||||
}
|
||||
|
||||
saveFocusedElement();
|
||||
-
|
||||
|
||||
$dialog.modal();
|
||||
|
||||
$dialog.on('hidden.bs.modal', () => {
|
||||
|
||||
@@ -15,6 +15,20 @@ const TPL = `
|
||||
overflow-y: auto;
|
||||
max-height: 85vh;
|
||||
}
|
||||
|
||||
.options-dialog .options-section:first-of-type h4 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.options-dialog .options-section h4 {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.options-dialog .options-section h5 {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="modal-dialog modal-lg" style="min-width: 1000px;" role="document">
|
||||
@@ -27,51 +41,9 @@ const TPL = `
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="display: flex">
|
||||
<ul class="nav nav-tabs flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#options-appearance">Appearance</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#options-shortcuts">Shortcuts</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#options-text-notes">Text notes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#options-code-notes">Code notes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#options-password">Password</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#options-etapi">ETAPI</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#options-backup">Backup</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#options-sync-setup">Sync</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#options-other">Other</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#options-advanced">Advanced</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav nav-tabs flex-column"></ul>
|
||||
<br/>
|
||||
<div class="tab-content">
|
||||
<div id="options-appearance" class="tab-pane active"></div>
|
||||
<div id="options-shortcuts" class="tab-pane"></div>
|
||||
<div id="options-text-notes" class="tab-pane"></div>
|
||||
<div id="options-code-notes" class="tab-pane"></div>
|
||||
<div id="options-password" class="tab-pane"></div>
|
||||
<div id="options-etapi" class="tab-pane"></div>
|
||||
<div id="options-backup" class="tab-pane"></div>
|
||||
<div id="options-sync-setup" class="tab-pane"></div>
|
||||
<div id="options-other" class="tab-pane"></div>
|
||||
<div id="options-advanced" class="tab-pane"></div>
|
||||
</div>
|
||||
<div class="tab-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,34 +54,51 @@ const TPL = `
|
||||
export default class OptionsDialog extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$navTabs = this.$widget.find(".nav-tabs");
|
||||
this.$tabContent = this.$widget.find(".tab-content");
|
||||
|
||||
for (const child of this.children) {
|
||||
this.$navTabs.append(
|
||||
$('<li class="nav-item">')
|
||||
.append(
|
||||
$('<a class="nav-link" data-toggle="tab">')
|
||||
.attr("href", '#options-' + child.constructor.name)
|
||||
.text(child.tabTitle)
|
||||
)
|
||||
);
|
||||
|
||||
this.$tabContent.append(
|
||||
$('<div class="tab-pane">')
|
||||
.attr("id", "options-" + child.constructor.name)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async showOptionsEvent({openTab}) {
|
||||
const options = await server.get('options');
|
||||
const optionPromise = server.get('options');
|
||||
|
||||
utils.openDialog(this.$widget);
|
||||
for (const child of this.children) {
|
||||
child.lazyRender();
|
||||
|
||||
(await Promise.all([
|
||||
import('./options/appearance.js'),
|
||||
import('./options/shortcuts.js'),
|
||||
import('./options/text_notes.js'),
|
||||
import('./options/code_notes.js'),
|
||||
import('./options/password.js'),
|
||||
import('./options/etapi.js'),
|
||||
import('./options/backup.js'),
|
||||
import('./options/sync.js'),
|
||||
import('./options/other.js'),
|
||||
import('./options/advanced.js')
|
||||
]))
|
||||
.map(m => new m.default)
|
||||
.forEach(tab => {
|
||||
if (tab.optionsLoaded) {
|
||||
tab.optionsLoaded(options)
|
||||
}
|
||||
});
|
||||
|
||||
if (openTab) {
|
||||
$(`.nav-link[href='#options-${openTab}']`).trigger("click");
|
||||
this.$widget.find("#options-" + child.constructor.name)
|
||||
.empty()
|
||||
.append(child.$widget);
|
||||
}
|
||||
|
||||
const options = await optionPromise;
|
||||
|
||||
for (const child of this.children) {
|
||||
if (child.optionsLoaded) {
|
||||
child.optionsLoaded(options)
|
||||
}
|
||||
}
|
||||
|
||||
await utils.openDialog(this.$widget);
|
||||
|
||||
if (!openTab) {
|
||||
openTab = "AppearanceOptions";
|
||||
}
|
||||
|
||||
this.$widget.find(`.nav-link[href='#options-${openTab}']`).trigger("click");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,69 @@
|
||||
import server from "../../../services/server.js";
|
||||
import toastService from "../../../services/toast.js";
|
||||
import OptionsTab from "./options_tab.js";
|
||||
|
||||
const TPL = `
|
||||
<h4 style="margin-top: 0;">Sync</h4>
|
||||
<button id="force-full-sync-button" class="btn">Force full sync</button>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<button id="fill-entity-changes-button" class="btn">Fill entity changes records</button>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<h4>Database integrity check</h4>
|
||||
|
||||
<p>This will check that the database is not corrupted on the SQLite level. It might take some time, depending on the DB size.</p>
|
||||
|
||||
<button id="check-integrity-button" class="btn">Check database integrity</button><br/><br/>
|
||||
|
||||
<h4>Consistency checks</h4>
|
||||
|
||||
<button id="find-and-fix-consistency-issues-button" class="btn">Find and fix consistency issues</button><br/><br/>
|
||||
|
||||
<h4>Anonymize database</h4>
|
||||
|
||||
<h5>Full anonymization</h5>
|
||||
|
||||
<p>This action will create a new copy of the database and anonymize it (remove all note content and leave only structure and some non-sensitive metadata)
|
||||
for sharing online for debugging purposes without fear of leaking your personal data.</p>
|
||||
|
||||
<button id="anonymize-full-button" class="btn">Save fully anonymized database</button><br/><br/>
|
||||
<div class="options-section">
|
||||
<h4>Sync</h4>
|
||||
<button id="force-full-sync-button" class="btn">Force full sync</button>
|
||||
|
||||
<h5>Light anonymization</h5>
|
||||
<button id="fill-entity-changes-button" class="btn">Fill entity changes records</button>
|
||||
</div>
|
||||
|
||||
<p>This action will create a new copy of the database and do a light anonymization on it - specifically only content of all notes will be removed, but titles and attributes will remain. Additionally, custom JS frontend/backend script notes and custom widgets will remain. This provides more context to debug the issues.</p>
|
||||
<div class="options-section">
|
||||
<h4>Database integrity check</h4>
|
||||
|
||||
<p>This will check that the database is not corrupted on the SQLite level. It might take some time, depending on the DB size.</p>
|
||||
|
||||
<button id="check-integrity-button" class="btn">Check database integrity</button>
|
||||
</div>
|
||||
|
||||
<p>You can decide yourself if you want to provide fully or lightly anonymized database. Even fully anonymized DB is very useful, however in some cases lightly anonymized database can speed up the process of bug identification and fixing.</p>
|
||||
<div class="options-section">
|
||||
<h4>Consistency checks</h4>
|
||||
|
||||
<button id="find-and-fix-consistency-issues-button" class="btn">Find and fix consistency issues</button>
|
||||
</div>
|
||||
|
||||
<button id="anonymize-light-button" class="btn">Save lightly anonymized database</button><br/><br/>
|
||||
<div class="options-section">
|
||||
<h4>Anonymize database</h4>
|
||||
|
||||
<h5>Full anonymization</h5>
|
||||
|
||||
<p>This action will create a new copy of the database and anonymize it (remove all note content and leave only structure and some non-sensitive metadata)
|
||||
for sharing online for debugging purposes without fear of leaking your personal data.</p>
|
||||
|
||||
<button id="anonymize-full-button" class="btn">Save fully anonymized database</button>
|
||||
|
||||
<h4>Vacuum database</h4>
|
||||
<h5>Light anonymization</h5>
|
||||
|
||||
<p>This action will create a new copy of the database and do a light anonymization on it - specifically only content of all notes will be removed, but titles and attributes will remain. Additionally, custom JS frontend/backend script notes and custom widgets will remain. This provides more context to debug the issues.</p>
|
||||
|
||||
<p>You can decide yourself if you want to provide fully or lightly anonymized database. Even fully anonymized DB is very useful, however in some cases lightly anonymized database can speed up the process of bug identification and fixing.</p>
|
||||
|
||||
<button id="anonymize-light-button" class="btn">Save lightly anonymized database</button>
|
||||
</div>
|
||||
|
||||
<p>This will rebuild the database which will typically result in a smaller database file. No data will be actually changed.</p>
|
||||
<div class="options-section">
|
||||
<h4>Vacuum database</h4>
|
||||
|
||||
<p>This will rebuild the database which will typically result in a smaller database file. No data will be actually changed.</p>
|
||||
|
||||
<button id="vacuum-database-button" class="btn">Vacuum database</button>
|
||||
</div>`;
|
||||
|
||||
<button id="vacuum-database-button" class="btn">Vacuum database</button>`;
|
||||
export default class AdvancedOptions extends OptionsTab {
|
||||
get tabTitle() { return "Advanced" }
|
||||
|
||||
export default class AdvancedOptions {
|
||||
constructor() {
|
||||
$("#options-advanced").html(TPL);
|
||||
lazyRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$forceFullSyncButton = $("#force-full-sync-button");
|
||||
this.$fillEntityChangesButton = $("#fill-entity-changes-button");
|
||||
this.$anonymizeFullButton = $("#anonymize-full-button");
|
||||
this.$anonymizeLightButton = $("#anonymize-light-button");
|
||||
this.$vacuumDatabaseButton = $("#vacuum-database-button");
|
||||
this.$findAndFixConsistencyIssuesButton = $("#find-and-fix-consistency-issues-button");
|
||||
this.$checkIntegrityButton = $("#check-integrity-button");
|
||||
this.$forceFullSyncButton = this.$widget.find("#force-full-sync-button");
|
||||
this.$fillEntityChangesButton = this.$widget.find("#fill-entity-changes-button");
|
||||
this.$anonymizeFullButton = this.$widget.find("#anonymize-full-button");
|
||||
this.$anonymizeLightButton = this.$widget.find("#anonymize-light-button");
|
||||
this.$vacuumDatabaseButton = this.$widget.find("#vacuum-database-button");
|
||||
this.$findAndFixConsistencyIssuesButton = this.$widget.find("#find-and-fix-consistency-issues-button");
|
||||
this.$checkIntegrityButton = this.$widget.find("#check-integrity-button");
|
||||
|
||||
this.$forceFullSyncButton.on('click', async () => {
|
||||
await server.post('sync/force-full-sync');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import server from "../../../services/server.js";
|
||||
import utils from "../../../services/utils.js";
|
||||
import appContext from "../../../services/app_context.js";
|
||||
import OptionsTab from "./options_tab.js";
|
||||
|
||||
const FONT_FAMILIES = [
|
||||
{ value: "theme", label: "Theme defined" },
|
||||
@@ -30,17 +31,25 @@ const FONT_FAMILIES = [
|
||||
const TPL = `
|
||||
<p><strong>Settings on this options tab are saved automatically after each change.</strong></p>
|
||||
|
||||
<form>
|
||||
<style>
|
||||
.options-section .row {
|
||||
/* rows otherwise overflow horizontally and force a scrollbar */
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="options-section">
|
||||
<div class="form-group row">
|
||||
<div class="col-6">
|
||||
<label for="zoom-factor-select">Zoom factor (desktop build only)</label>
|
||||
|
||||
|
||||
<input type="number" class="form-control" id="zoom-factor-select" min="0.3" max="2.0" step="0.1"/>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<label for="native-title-bar-select">Native title bar (requires app restart)</label>
|
||||
|
||||
|
||||
<select class="form-control" id="native-title-bar-select">
|
||||
<option value="show">enabled</option>
|
||||
<option value="hide">disabled</option>
|
||||
@@ -49,9 +58,11 @@ const TPL = `
|
||||
</div>
|
||||
|
||||
<p>Zooming can be controlled with CTRL+- and CTRL+= shortcuts as well.</p>
|
||||
</div>
|
||||
|
||||
<div class="options-section">
|
||||
<h4>Theme</h4>
|
||||
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-6">
|
||||
<label for="theme-select">Theme</label>
|
||||
@@ -63,104 +74,106 @@ const TPL = `
|
||||
<input type="checkbox" class="form-control" id="override-theme-fonts">
|
||||
</div>
|
||||
</div>
|
||||
<div class="options-section">
|
||||
|
||||
<div id="overriden-font-settings">
|
||||
<h4>Fonts</h4>
|
||||
|
||||
<h5>Main font</h5>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-6">
|
||||
<label for="main-font-family">Font family</label>
|
||||
<select class="form-control" id="main-font-family"></select>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<label for="main-font-size">Size</label>
|
||||
<div id="overriden-font-settings" class="options-section">
|
||||
<h4>Fonts</h4>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="main-font-size" min="50" max="200" step="10"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
<h5>Main font</h5>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-6">
|
||||
<label for="main-font-family">Font family</label>
|
||||
<select class="form-control" id="main-font-family"></select>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<label for="main-font-size">Size</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="main-font-size" min="50" max="200" step="10"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>Note tree font</h5>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-4">
|
||||
<label for="tree-font-family">Font family</label>
|
||||
<select class="form-control" id="tree-font-family"></select>
|
||||
</div>
|
||||
|
||||
<h5>Note tree font</h5>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-4">
|
||||
<label for="tree-font-family">Font family</label>
|
||||
<select class="form-control" id="tree-font-family"></select>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<label for="tree-font-size">Size</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="tree-font-size" min="50" max="200" step="10"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label for="tree-font-size">Size</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="tree-font-size" min="50" max="200" step="10"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>Note detail font</h5>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-4">
|
||||
<label for="detail-font-family">Font family</label>
|
||||
<select class="form-control" id="detail-font-family"></select>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<label for="detail-font-size">Size</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="detail-font-size" min="50" max="200" step="10"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>Monospace (code) font</h5>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-4">
|
||||
<label for="monospace-font-family">Font family</label>
|
||||
<select class="form-control" id="monospace-font-family"></select>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<label for="monospace-font-size">Size</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="monospace-font-size" min="50" max="200" step="10"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Note that tree and detail font sizing is relative to the main font size setting.</p>
|
||||
|
||||
<p>Not all listed fonts may be available on your system.</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
To apply font changes, click on
|
||||
<button class="btn btn-micro reload-frontend-button">reload frontend</button>
|
||||
</p>
|
||||
<h5>Note detail font</h5>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-4">
|
||||
<label for="detail-font-family">Font family</label>
|
||||
<select class="form-control" id="detail-font-family"></select>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<label for="detail-font-size">Size</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="detail-font-size" min="50" max="200" step="10"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>Monospace (code) font</h5>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-4">
|
||||
<label for="monospace-font-family">Font family</label>
|
||||
<select class="form-control" id="monospace-font-family"></select>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<label for="monospace-font-size">Size</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="monospace-font-size" min="50" max="200" step="10"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Note that tree and detail font sizing is relative to the main font size setting.</p>
|
||||
|
||||
<p>Not all listed fonts may be available on your system.</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
To apply font changes, click on
|
||||
<button class="btn btn-micro reload-frontend-button">reload frontend</button>
|
||||
</p>
|
||||
|
||||
<div class="options-section">
|
||||
<h4>Content width</h4>
|
||||
|
||||
<p>Trilium by default limits max content width to improve readability for maximized screens on wide screens.</p>
|
||||
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-4">
|
||||
<label for="max-content-width">Max content width in pixels</label>
|
||||
@@ -172,35 +185,38 @@ const TPL = `
|
||||
To content width changes, click on
|
||||
<button class="btn btn-micro reload-frontend-button">reload frontend</button>
|
||||
</p>
|
||||
</form>`;
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class ApperanceOptions {
|
||||
constructor() {
|
||||
$("#options-appearance").html(TPL);
|
||||
export default class AppearanceOptions extends OptionsTab {
|
||||
get tabTitle() { return "Appearance" }
|
||||
|
||||
this.$zoomFactorSelect = $("#zoom-factor-select");
|
||||
this.$nativeTitleBarSelect = $("#native-title-bar-select");
|
||||
lazyRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$themeSelect = $("#theme-select");
|
||||
this.$overrideThemeFonts = $("#override-theme-fonts");
|
||||
this.$zoomFactorSelect = this.$widget.find("#zoom-factor-select");
|
||||
this.$nativeTitleBarSelect = this.$widget.find("#native-title-bar-select");
|
||||
|
||||
this.$overridenFontSettings = $("#overriden-font-settings");
|
||||
this.$themeSelect = this.$widget.find("#theme-select");
|
||||
this.$overrideThemeFonts = this.$widget.find("#override-theme-fonts");
|
||||
|
||||
this.$mainFontSize = $("#main-font-size");
|
||||
this.$mainFontFamily = $("#main-font-family");
|
||||
this.$overridenFontSettings = this.$widget.find("#overriden-font-settings");
|
||||
|
||||
this.$treeFontSize = $("#tree-font-size");
|
||||
this.$treeFontFamily = $("#tree-font-family");
|
||||
this.$mainFontSize = this.$widget.find("#main-font-size");
|
||||
this.$mainFontFamily = this.$widget.find("#main-font-family");
|
||||
|
||||
this.$detailFontSize = $("#detail-font-size");
|
||||
this.$detailFontFamily = $("#detail-font-family");
|
||||
this.$treeFontSize = this.$widget.find("#tree-font-size");
|
||||
this.$treeFontFamily = this.$widget.find("#tree-font-family");
|
||||
|
||||
this.$monospaceFontSize = $("#monospace-font-size");
|
||||
this.$monospaceFontFamily = $("#monospace-font-family");
|
||||
this.$detailFontSize = this.$widget.find("#detail-font-size");
|
||||
this.$detailFontFamily = this.$widget.find("#detail-font-family");
|
||||
|
||||
$(".reload-frontend-button").on("click", () => utils.reloadFrontendApp("changes from appearance options"));
|
||||
this.$monospaceFontSize = this.$widget.find("#monospace-font-size");
|
||||
this.$monospaceFontFamily = this.$widget.find("#monospace-font-family");
|
||||
|
||||
this.$body = $("body");
|
||||
this.$widget.find(".reload-frontend-button").on("click", () => utils.reloadFrontendApp("changes from appearance options"));
|
||||
|
||||
this.$body = this.$widget.find("body");
|
||||
|
||||
this.$themeSelect.on('change', async () => {
|
||||
const newTheme = this.$themeSelect.val();
|
||||
@@ -211,11 +227,9 @@ export default class ApperanceOptions {
|
||||
});
|
||||
|
||||
this.$overrideThemeFonts.on('change', async () => {
|
||||
const isOverriden = this.$overrideThemeFonts.is(":checked");
|
||||
this.updateCheckboxOption('overrideThemeFonts', this.$overrideThemeFonts);
|
||||
|
||||
await server.put('options/overrideThemeFonts/' + isOverriden.toString());
|
||||
|
||||
this.$overridenFontSettings.toggle(isOverriden);
|
||||
this.$overridenFontSettings.toggle(this.$overrideThemeFonts.is(":checked"));
|
||||
});
|
||||
|
||||
this.$zoomFactorSelect.on('change', () => { appContext.triggerCommand('setZoomFactorAndSave', {zoomFactor: this.$zoomFactorSelect.val()}); });
|
||||
@@ -223,7 +237,7 @@ export default class ApperanceOptions {
|
||||
this.$nativeTitleBarSelect.on('change', () => {
|
||||
const nativeTitleBarVisible = this.$nativeTitleBarSelect.val() === 'show' ? 'true' : 'false';
|
||||
|
||||
server.put('options/nativeTitleBarVisible/' + nativeTitleBarVisible);
|
||||
this.updateOption('nativeTitleBarVisible', nativeTitleBarVisible);
|
||||
});
|
||||
|
||||
const optionsToSave = [
|
||||
@@ -234,16 +248,14 @@ export default class ApperanceOptions {
|
||||
];
|
||||
|
||||
for (const optionName of optionsToSave) {
|
||||
this['$' + optionName].on('change', () => server.put(`options/${optionName}/${this['$' + optionName].val()}`));
|
||||
this['$' + optionName].on('change', () =>
|
||||
this.updateOption(optionName, this['$' + optionName].val()));
|
||||
}
|
||||
|
||||
this.$maxContentWidth = $("#max-content-width");
|
||||
this.$maxContentWidth = this.$widget.find("#max-content-width");
|
||||
|
||||
this.$maxContentWidth.on('change', async () => {
|
||||
const maxContentWidth = this.$maxContentWidth.val();
|
||||
|
||||
await server.put('options/maxContentWidth/' + maxContentWidth);
|
||||
})
|
||||
this.$maxContentWidth.on('change', async () =>
|
||||
this.updateOption('maxContentWidth', this.$maxContentWidth.val()))
|
||||
}
|
||||
|
||||
toggleBodyClass(prefix, value) {
|
||||
@@ -282,7 +294,7 @@ export default class ApperanceOptions {
|
||||
|
||||
this.$themeSelect.val(options.theme);
|
||||
|
||||
this.$overrideThemeFonts.prop('checked', options.overrideThemeFonts === 'true');
|
||||
this.setCheckboxState(this.$overrideThemeFonts, options.overrideThemeFonts);
|
||||
this.$overridenFontSettings.toggle(options.overrideThemeFonts === 'true');
|
||||
|
||||
this.$mainFontSize.val(options.mainFontSize);
|
||||
|
||||
@@ -1,42 +1,47 @@
|
||||
import server from "../../../services/server.js";
|
||||
import toastService from "../../../services/toast.js";
|
||||
import OptionsTab from "./options_tab.js";
|
||||
|
||||
const TPL = `
|
||||
<h4>Automatic backup</h4>
|
||||
|
||||
<p>Trilium can back up the database automatically:</p>
|
||||
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="daily-backup-enabled">
|
||||
<label class="custom-control-label" for="daily-backup-enabled">Enable daily backup</label>
|
||||
<div class="options-section">
|
||||
<h4>Automatic backup</h4>
|
||||
|
||||
<p>Trilium can back up the database automatically:</p>
|
||||
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="daily-backup-enabled">
|
||||
<label class="custom-control-label" for="daily-backup-enabled">Enable daily backup</label>
|
||||
</div>
|
||||
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="weekly-backup-enabled">
|
||||
<label class="custom-control-label" for="weekly-backup-enabled">Enable weekly backup</label>
|
||||
</div>
|
||||
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="monthly-backup-enabled">
|
||||
<label class="custom-control-label" for="monthly-backup-enabled">Enable monthly backup</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<p>It's recommended to keep the backup turned on, but this can make application startup slow with large databases and/or slow storage devices.</p>
|
||||
</div>
|
||||
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="weekly-backup-enabled">
|
||||
<label class="custom-control-label" for="weekly-backup-enabled">Enable weekly backup</label>
|
||||
<div class="options-section">
|
||||
<h4>Backup now</h4>
|
||||
|
||||
<button id="backup-database-button" class="btn">Backup database now</button>
|
||||
</div>
|
||||
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="monthly-backup-enabled">
|
||||
<label class="custom-control-label" for="monthly-backup-enabled">Enable monthly backup</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<p>It's recommended to keep the backup turned on, but this can make application startup slow with large databases and/or slow storage devices.</p>
|
||||
|
||||
<br/>
|
||||
|
||||
<h4>Backup now</h4>
|
||||
|
||||
<button id="backup-database-button" class="btn">Backup database now</button><br/><br/>
|
||||
`;
|
||||
|
||||
export default class BackupOptions {
|
||||
constructor() {
|
||||
$("#options-backup").html(TPL);
|
||||
export default class BackupOptions extends OptionsTab {
|
||||
get tabTitle() { return "Backup" }
|
||||
|
||||
this.$backupDatabaseButton = $("#backup-database-button");
|
||||
lazyRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$backupDatabaseButton = this.$widget.find("#backup-database-button");
|
||||
|
||||
this.$backupDatabaseButton.on('click', async () => {
|
||||
const {backupFile} = await server.post('database/backup-database');
|
||||
@@ -44,35 +49,23 @@ export default class BackupOptions {
|
||||
toastService.showMessage("Database has been backed up to " + backupFile, 10000);
|
||||
});
|
||||
|
||||
this.$dailyBackupEnabled = $("#daily-backup-enabled");
|
||||
this.$weeklyBackupEnabled = $("#weekly-backup-enabled");
|
||||
this.$monthlyBackupEnabled = $("#monthly-backup-enabled");
|
||||
this.$dailyBackupEnabled = this.$widget.find("#daily-backup-enabled");
|
||||
this.$weeklyBackupEnabled = this.$widget.find("#weekly-backup-enabled");
|
||||
this.$monthlyBackupEnabled = this.$widget.find("#monthly-backup-enabled");
|
||||
|
||||
this.$dailyBackupEnabled.on('change', () => {
|
||||
const opts = { 'dailyBackupEnabled': this.$dailyBackupEnabled.is(":checked") ? "true" : "false" };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
this.$dailyBackupEnabled.on('change', () =>
|
||||
this.updateCheckboxOption('dailyBackupEnabled', this.$dailyBackupEnabled));
|
||||
|
||||
return false;
|
||||
});
|
||||
this.$weeklyBackupEnabled.on('change', () =>
|
||||
this.updateCheckboxOption('weeklyBackupEnabled', this.$weeklyBackupEnabled));
|
||||
|
||||
this.$weeklyBackupEnabled.on('change', () => {
|
||||
const opts = { 'weeklyBackupEnabled': this.$weeklyBackupEnabled.is(":checked") ? "true" : "false" };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$monthlyBackupEnabled.on('change', () => {
|
||||
const opts = { 'monthlyBackupEnabled': this.$monthlyBackupEnabled.is(":checked") ? "true" : "false" };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
this.$monthlyBackupEnabled.on('change', () =>
|
||||
this.updateCheckboxOption('monthlyBackupEnabled', this.$monthlyBackupEnabled));
|
||||
}
|
||||
|
||||
optionsLoaded(options) {
|
||||
this.$dailyBackupEnabled.prop("checked", options['dailyBackupEnabled'] === 'true');
|
||||
this.$weeklyBackupEnabled.prop("checked", options['weeklyBackupEnabled'] === 'true');
|
||||
this.$monthlyBackupEnabled.prop("checked", options['monthlyBackupEnabled'] === 'true');
|
||||
this.setCheckboxState(this.$dailyBackupEnabled, options.dailyBackupEnabled);
|
||||
this.setCheckboxState(this.$weeklyBackupEnabled, options.weeklyBackupEnabled);
|
||||
this.setCheckboxState(this.$monthlyBackupEnabled, options.monthlyBackupEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,49 +2,69 @@ import mimeTypesService from "../../../services/mime_types.js";
|
||||
import options from "../../../services/options.js";
|
||||
import server from "../../../services/server.js";
|
||||
import toastService from "../../../services/toast.js";
|
||||
import OptionsTab from "./options_tab.js";
|
||||
|
||||
const TPL = `
|
||||
<h4>Use vim keybindings in code notes (no ex mode)</h4>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="vim-keymap-enabled">
|
||||
<label class="custom-control-label" for="vim-keymap-enabled">Enable Vim Keybindings</label>
|
||||
<div class="options-section">
|
||||
<h4>Use vim keybindings in code notes (no ex mode)</h4>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="vim-keymap-enabled">
|
||||
<label class="custom-control-label" for="vim-keymap-enabled">Enable Vim Keybindings</label>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<h4>Wrap lines in code notes</h4>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="line-wrap-enabled">
|
||||
<label class="custom-control-label" for="line-wrap-enabled">Enable Line Wrap (change might need a frontend reload to take effect)</label>
|
||||
<div class="options-section">
|
||||
<h4>Wrap lines in code notes</h4>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="line-wrap-enabled">
|
||||
<label class="custom-control-label" for="line-wrap-enabled">Enable Line Wrap (change might need a frontend reload to take effect)</label>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<h4>Available MIME types in the dropdown</h4>
|
||||
<div class="options-section">
|
||||
<h4>Automatic readonly size</h4>
|
||||
|
||||
<ul id="options-mime-types" style="max-height: 500px; overflow: auto; list-style-type: none;"></ul>`;
|
||||
<p>Automatic readonly note size is the size after which notes will be displayed in a readonly mode (for performance reasons).</p>
|
||||
|
||||
export default class CodeNotesOptions {
|
||||
constructor() {
|
||||
$("#options-code-notes").html(TPL);
|
||||
<div class="form-group">
|
||||
<label for="auto-readonly-size-code">Automatic readonly size (code notes)</label>
|
||||
<input class="form-control" id="auto-readonly-size-code" type="number" min="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
this.$vimKeymapEnabled = $("#vim-keymap-enabled");
|
||||
this.$vimKeymapEnabled.on('change', () => {
|
||||
const opts = { 'vimKeymapEnabled': this.$vimKeymapEnabled.is(":checked") ? "true" : "false" };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
|
||||
return false;
|
||||
});
|
||||
this.$codeLineWrapEnabled = $("#line-wrap-enabled");
|
||||
this.$codeLineWrapEnabled.on('change', () => {
|
||||
const opts = { 'codeLineWrapEnabled': this.$codeLineWrapEnabled.is(":checked") ? "true" : "false" };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
|
||||
return false;
|
||||
});
|
||||
this.$mimeTypes = $("#options-mime-types");
|
||||
<div class="options-section">
|
||||
<h4>Available MIME types in the dropdown</h4>
|
||||
|
||||
<ul id="options-mime-types" style="max-height: 500px; overflow: auto; list-style-type: none;"></ul>
|
||||
</div>`;
|
||||
|
||||
export default class CodeNotesOptions extends OptionsTab {
|
||||
get tabTitle() { return "Code notes" }
|
||||
|
||||
lazyRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$vimKeymapEnabled = this.$widget.find("#vim-keymap-enabled");
|
||||
this.$vimKeymapEnabled.on('change', () =>
|
||||
this.updateCheckboxOption('vimKeymapEnabled', this.$vimKeymapEnabled));
|
||||
|
||||
this.$codeLineWrapEnabled = this.$widget.find("#line-wrap-enabled");
|
||||
this.$codeLineWrapEnabled.on('change', () =>
|
||||
this.updateCheckboxOption('codeLineWrapEnabled', this.$codeLineWrapEnabled));
|
||||
|
||||
this.$mimeTypes = this.$widget.find("#options-mime-types");
|
||||
|
||||
this.$autoReadonlySizeCode = this.$widget.find("#auto-readonly-size-code");
|
||||
this.$autoReadonlySizeCode.on('change', () =>
|
||||
this.updateOption('autoReadonlySizeCode', this.$autoReadonlySizeCode.val()));
|
||||
}
|
||||
|
||||
async optionsLoaded(options) {
|
||||
this.$mimeTypes.empty();
|
||||
this.$vimKeymapEnabled.prop("checked", options['vimKeymapEnabled'] === 'true');
|
||||
this.$codeLineWrapEnabled.prop("checked", options['codeLineWrapEnabled'] === 'true');
|
||||
this.setCheckboxState(this.$vimKeymapEnabled, options.vimKeymapEnabled);
|
||||
this.setCheckboxState(this.$codeLineWrapEnabled, options.codeLineWrapEnabled);
|
||||
this.$autoReadonlySizeCode.val(options.autoReadonlySizeCode);
|
||||
|
||||
let idCtr = 1;
|
||||
|
||||
for (const mimeType of await mimeTypesService.getMimeTypes()) {
|
||||
@@ -68,9 +88,9 @@ export default class CodeNotesOptions {
|
||||
const enabledMimeTypes = [];
|
||||
|
||||
this.$mimeTypes.find("input:checked").each(
|
||||
(i, el) => enabledMimeTypes.push($(el).attr("data-mime-type")));
|
||||
(i, el) => enabledMimeTypes.push(this.$widget.find(el).attr("data-mime-type")));
|
||||
|
||||
await options.save('codeNotesMimeTypes', JSON.stringify(enabledMimeTypes));
|
||||
await this.updateOption('codeNotesMimeTypes', JSON.stringify(enabledMimeTypes));
|
||||
|
||||
mimeTypesService.loadMimeTypes();
|
||||
}
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
import server from "../../../services/server.js";
|
||||
import dialogService from "../../dialog.js";
|
||||
import toastService from "../../../services/toast.js";
|
||||
import OptionsTab from "./options_tab.js";
|
||||
|
||||
const TPL = `
|
||||
<h4>ETAPI</h4>
|
||||
<div class="options-section">
|
||||
<h4>ETAPI</h4>
|
||||
|
||||
<p>ETAPI is a REST API used to access Trilium instance programmatically, without UI. <br/>
|
||||
See more details on <a href="https://github.com/zadam/trilium/wiki/ETAPI">wiki</a> and <a onclick="window.open('etapi/etapi.openapi.yaml')" href="etapi/etapi.openapi.yaml">ETAPI OpenAPI spec</a>.</p>
|
||||
|
||||
<button type="button" class="btn btn-sm" id="create-etapi-token">Create new ETAPI token</button>
|
||||
|
||||
<p>ETAPI is a REST API used to access Trilium instance programmatically, without UI. <br/>
|
||||
See more details on <a href="https://github.com/zadam/trilium/wiki/ETAPI">wiki</a> and <a onclick="window.open('etapi/etapi.openapi.yaml')" href="etapi/etapi.openapi.yaml">ETAPI OpenAPI spec</a>.</p>
|
||||
|
||||
<button type="button" class="btn btn-sm" id="create-etapi-token">Create new ETAPI token</button>
|
||||
|
||||
<br/><br/>
|
||||
|
||||
<h5>Existing tokens</h5>
|
||||
|
||||
<div id="no-tokens-yet">There are no tokens yet. Click on the button above to create one.</div>
|
||||
|
||||
<div style="overflow: auto; height: 500px;">
|
||||
<table id="tokens-table" class="table table-stripped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Token name</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<h5>Existing tokens</h5>
|
||||
|
||||
<div id="no-tokens-yet">There are no tokens yet. Click on the button above to create one.</div>
|
||||
|
||||
<div style="overflow: auto; height: 500px;">
|
||||
<table id="tokens-table" class="table table-stripped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Token name</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -45,11 +46,13 @@ const TPL = `
|
||||
}
|
||||
</style>`;
|
||||
|
||||
export default class EtapiOptions {
|
||||
constructor() {
|
||||
$("#options-etapi").html(TPL);
|
||||
export default class EtapiOptions extends OptionsTab {
|
||||
get tabTitle() { return "ETAPI" }
|
||||
|
||||
$("#create-etapi-token").on("click", async () => {
|
||||
lazyRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$widget.find("#create-etapi-token").on("click", async () => {
|
||||
const tokenName = await dialogService.prompt({
|
||||
title: "New ETAPI token",
|
||||
message: "Please enter new token's name",
|
||||
@@ -76,8 +79,8 @@ export default class EtapiOptions {
|
||||
}
|
||||
|
||||
async refreshTokens() {
|
||||
const $noTokensYet = $("#no-tokens-yet");
|
||||
const $tokensTable = $("#tokens-table");
|
||||
const $noTokensYet = this.$widget.find("#no-tokens-yet");
|
||||
const $tokensTable = this.$widget.find("#tokens-table");
|
||||
|
||||
const tokens = await server.get('etapi-tokens');
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import OptionsTab from "./options_tab.js";
|
||||
|
||||
const TPL = `
|
||||
<style>
|
||||
.options-section .disabled-field {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="options-section">
|
||||
<h4>Images</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<input id="download-images-automatically" type="checkbox" name="download-images-automatically">
|
||||
<label for="download-images-automatically">Download images automatically for offline use.</label>
|
||||
<p>(pasted HTML can contain references to online images, Trilium will find those references and download the images so that they are available offline)</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<input id="image-compresion-enabled" type="checkbox" name="image-compression-enabled">
|
||||
<label for="image-compresion-enabled">Enable image compression</label>
|
||||
</div>
|
||||
|
||||
<div id="image-compression-enabled-wraper">
|
||||
<div class="form-group">
|
||||
<label for="image-max-width-height">Max width / height of an image in pixels (image will be resized if it exceeds this setting).</label>
|
||||
<input class="form-control" id="image-max-width-height" type="number" min="1">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="image-jpeg-quality">JPEG quality (10 - worst quality, 100 best quality, 50 - 85 is recommended)</label>
|
||||
<input class="form-control" id="image-jpeg-quality" min="10" max="100" type="number">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class ImageOptions extends OptionsTab {
|
||||
get tabTitle() { return "Images" }
|
||||
|
||||
lazyRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$imageMaxWidthHeight = this.$widget.find("#image-max-width-height");
|
||||
this.$imageJpegQuality = this.$widget.find("#image-jpeg-quality");
|
||||
|
||||
this.$imageMaxWidthHeight.on('change', () =>
|
||||
this.updateOption('imageMaxWidthHeight', this.$imageMaxWidthHeight.val()));
|
||||
|
||||
this.$imageJpegQuality.on('change', () =>
|
||||
this.updateOption('imageJpegQuality', this.$imageJpegQuality.val()));
|
||||
|
||||
this.$downloadImagesAutomatically = this.$widget.find("#download-images-automatically");
|
||||
|
||||
this.$downloadImagesAutomatically.on("change", () =>
|
||||
this.updateCheckboxOption('downloadImagesAutomatically', this.$downloadImagesAutomatically));
|
||||
|
||||
this.$enableImageCompression = this.$widget.find("#image-compresion-enabled");
|
||||
this.$imageCompressionWrapper = this.$widget.find("#image-compression-enabled-wraper");
|
||||
|
||||
this.$enableImageCompression.on("change", () => {
|
||||
this.updateCheckboxOption('compressImages', this.$enableImageCompression);
|
||||
this.setImageCompression();
|
||||
});
|
||||
}
|
||||
|
||||
optionsLoaded(options) {
|
||||
this.$imageMaxWidthHeight.val(options.imageMaxWidthHeight);
|
||||
this.$imageJpegQuality.val(options.imageJpegQuality);
|
||||
|
||||
this.setCheckboxState(this.$downloadImagesAutomatically, options.downloadImagesAutomatically);
|
||||
this.setCheckboxState(this.$enableImageCompression, options.compressImages);
|
||||
|
||||
this.setImageCompression();
|
||||
}
|
||||
|
||||
setImageCompression() {
|
||||
if (this.$enableImageCompression.prop("checked")) {
|
||||
this.$imageCompressionWrapper.removeClass("disabled-field");
|
||||
} else {
|
||||
this.$imageCompressionWrapper.addClass("disabled-field");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import BasicWidget from "../../basic_widget.js";
|
||||
import server from "../../../services/server.js";
|
||||
import toastService from "../../../services/toast.js";
|
||||
|
||||
export default class OptionsTab extends BasicWidget {
|
||||
async updateOption(name, value) {
|
||||
const opts = { [name]: value };
|
||||
|
||||
await this.updateMultipleOptions(opts);
|
||||
}
|
||||
|
||||
async updateMultipleOptions(opts) {
|
||||
await server.put('options', opts);
|
||||
|
||||
this.showUpdateNotification();
|
||||
}
|
||||
|
||||
showUpdateNotification() {
|
||||
toastService.showPersistent({
|
||||
id: "options-change-saved",
|
||||
title: "Options status",
|
||||
message: "Options change have been saved.",
|
||||
icon: "slider",
|
||||
closeAfter: 2000
|
||||
});
|
||||
}
|
||||
|
||||
async updateCheckboxOption(name, $checkbox) {
|
||||
const isChecked = $checkbox.prop("checked");
|
||||
|
||||
return await this.updateOption(name, isChecked ? 'true' : 'false');
|
||||
}
|
||||
|
||||
setCheckboxState($checkbox, optionValue) {
|
||||
$checkbox.prop('checked', optionValue === 'true');
|
||||
}
|
||||
}
|
||||
@@ -1,65 +1,18 @@
|
||||
import utils from "../../../services/utils.js";
|
||||
import server from "../../../services/server.js";
|
||||
import toastService from "../../../services/toast.js";
|
||||
import OptionsTab from "./options_tab.js";
|
||||
|
||||
const TPL = `
|
||||
<style>
|
||||
.disabled-field {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<h4>Spell check</h4>
|
||||
|
||||
<p>These options apply only for desktop builds, browsers will use their own native spell check. App restart is required after change.</p>
|
||||
<div class="options-section">
|
||||
<h4>Tray</h4>
|
||||
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="spell-check-enabled">
|
||||
<label class="custom-control-label" for="spell-check-enabled">Enable spellcheck</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="spell-check-language-code">Language code(s)</label>
|
||||
<input type="text" class="form-control" id="spell-check-language-code" placeholder="for example "en-US", "de-AT"">
|
||||
</div>
|
||||
|
||||
<p>Multiple languages can be separated by comma, e.g. <code>en-US, de-DE, cs</code>. Changes to the spell check options will take effect after application restart.</p>
|
||||
|
||||
<p><strong>Available language codes: </strong> <span id="available-language-codes"></span></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Images</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<input id="download-images-automatically" type="checkbox" name="download-images-automatically">
|
||||
<label for="download-images-automatically">Download images automatically for offline use.</label>
|
||||
<p>(pasted HTML can contain references to online images, Trilium will find those references and download the images so that they are available offline)</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<input id="image-compresion-enabled" type="checkbox" name="image-compression-enabled">
|
||||
<label for="image-compresion-enabled">Enable image compression</label>
|
||||
</div>
|
||||
|
||||
<div id="image-compression-enabled-wraper">
|
||||
<div class="form-group">
|
||||
<label for="image-max-width-height">Max width / height of an image in pixels (image will be resized if it exceeds this setting).</label>
|
||||
<input class="form-control" id="image-max-width-height" type="number" min="1">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="image-jpeg-quality">JPEG quality (10 - worst quality, 100 best quality, 50 - 85 is recommended)</label>
|
||||
<input class="form-control" id="image-jpeg-quality" min="10" max="100" type="number">
|
||||
</div>
|
||||
<input type="checkbox" class="custom-control-input" id="tray-enabled">
|
||||
<label class="custom-control-label" for="tray-enabled">Enable tray (Trilium needs to be restarted for this change to take effect)</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="options-section">
|
||||
<h4>Note erasure timeout</h4>
|
||||
|
||||
<p>Deleted notes (and attributes, revisions...) are at first only marked as deleted and it is possible to recover them
|
||||
@@ -75,23 +28,9 @@ const TPL = `
|
||||
<p>You can also trigger erasing manually:</p>
|
||||
|
||||
<button id="erase-deleted-notes-now-button" class="btn">Erase deleted notes now</button>
|
||||
|
||||
<br/><br/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Protected session timeout</h4>
|
||||
|
||||
<p>Protected session timeout is a time period after which the protected session is wiped from
|
||||
the browser's memory. This is measured from the last interaction with protected notes. See <a href="https://github.com/zadam/trilium/wiki/Protected-notes" class="external">wiki</a> for more info.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="protected-session-timeout-in-seconds">Protected session timeout (in seconds)</label>
|
||||
<input class="form-control" id="protected-session-timeout-in-seconds" type="number" min="60">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="options-section">
|
||||
<h4>Note revisions snapshot interval</h4>
|
||||
|
||||
<p>Note revision snapshot time interval is time in seconds after which a new note revision will be created for the note. See <a href="https://github.com/zadam/trilium/wiki/Note-revisions" class="external">wiki</a> for more info.</p>
|
||||
@@ -102,198 +41,51 @@ const TPL = `
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Automatic readonly size</h4>
|
||||
|
||||
<p>Automatic readonly note size is the size after which notes will be displayed in a readonly mode (for performance reasons).</p>
|
||||
|
||||
<div class="options-section">
|
||||
<h4>Network connections</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="auto-readonly-size-text">Automatic readonly size (text notes)</label>
|
||||
<input class="form-control" id="auto-readonly-size-text" type="number" min="0">
|
||||
<input id="check-for-updates" type="checkbox" name="check-for-updates">
|
||||
<label for="check-for-updates">Check for updates automatically</label>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
<div class="form-group">
|
||||
<label for="auto-readonly-size-code">Automatic readonly size (code notes)</label>
|
||||
<input class="form-control" id="auto-readonly-size-code" type="number" min="0">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Network connections</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<input id="check-for-updates" type="checkbox" name="check-for-updates">
|
||||
<label for="check-for-updates">Check for updates automatically</label>
|
||||
</div>
|
||||
</div>
|
||||
export default class OtherOptions extends OptionsTab {
|
||||
get tabTitle() { return "Other" }
|
||||
|
||||
`;
|
||||
lazyRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
export default class ProtectedSessionOptions {
|
||||
constructor() {
|
||||
$("#options-other").html(TPL);
|
||||
this.$trayEnabled = this.$widget.find("#tray-enabled");
|
||||
this.$trayEnabled.on('change', () =>
|
||||
this.updateOption('disableTray', !this.$trayEnabled.is(":checked") ? "true" : "false"));
|
||||
|
||||
this.$spellCheckEnabled = $("#spell-check-enabled");
|
||||
this.$spellCheckLanguageCode = $("#spell-check-language-code");
|
||||
this.$eraseEntitiesAfterTimeInSeconds = this.$widget.find("#erase-entities-after-time-in-seconds");
|
||||
this.$eraseEntitiesAfterTimeInSeconds.on('change', () => this.updateOption('eraseEntitiesAfterTimeInSeconds', this.$eraseEntitiesAfterTimeInSeconds.val()));
|
||||
|
||||
this.$spellCheckEnabled.on('change', () => {
|
||||
const opts = { 'spellCheckEnabled': this.$spellCheckEnabled.is(":checked") ? "true" : "false" };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$spellCheckLanguageCode.on('change', () => {
|
||||
const opts = { 'spellCheckLanguageCode': this.$spellCheckLanguageCode.val() };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$availableLanguageCodes = $("#available-language-codes");
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const { webContents } = utils.dynamicRequire('@electron/remote').getCurrentWindow();
|
||||
|
||||
this.$availableLanguageCodes.text(webContents.session.availableSpellCheckerLanguages.join(', '));
|
||||
}
|
||||
|
||||
this.$eraseEntitiesAfterTimeInSeconds = $("#erase-entities-after-time-in-seconds");
|
||||
|
||||
this.$eraseEntitiesAfterTimeInSeconds.on('change', () => {
|
||||
const eraseEntitiesAfterTimeInSeconds = this.$eraseEntitiesAfterTimeInSeconds.val();
|
||||
|
||||
server.put('options', { 'eraseEntitiesAfterTimeInSeconds': eraseEntitiesAfterTimeInSeconds }).then(() => {
|
||||
toastService.showMessage("Options changed have been saved.");
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$eraseDeletedNotesButton = $("#erase-deleted-notes-now-button");
|
||||
this.$eraseDeletedNotesButton = this.$widget.find("#erase-deleted-notes-now-button");
|
||||
this.$eraseDeletedNotesButton.on('click', () => {
|
||||
server.post('notes/erase-deleted-notes-now').then(() => {
|
||||
toastService.showMessage("Deleted notes have been erased.");
|
||||
});
|
||||
});
|
||||
|
||||
this.$protectedSessionTimeout = $("#protected-session-timeout-in-seconds");
|
||||
this.$noteRevisionsTimeInterval = this.$widget.find("#note-revision-snapshot-time-interval-in-seconds");
|
||||
|
||||
this.$protectedSessionTimeout.on('change', () => {
|
||||
const protectedSessionTimeout = this.$protectedSessionTimeout.val();
|
||||
this.$noteRevisionsTimeInterval.on('change', () =>
|
||||
this.updateOption('noteRevisionSnapshotTimeInterval', this.$noteRevisionsTimeInterval.val()));
|
||||
|
||||
server.put('options', { 'protectedSessionTimeout': protectedSessionTimeout }).then(() => {
|
||||
toastService.showMessage("Options changed have been saved.");
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$noteRevisionsTimeInterval = $("#note-revision-snapshot-time-interval-in-seconds");
|
||||
|
||||
this.$noteRevisionsTimeInterval.on('change', () => {
|
||||
const opts = { 'noteRevisionSnapshotTimeInterval': this.$noteRevisionsTimeInterval.val() };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$imageMaxWidthHeight = $("#image-max-width-height");
|
||||
this.$imageJpegQuality = $("#image-jpeg-quality");
|
||||
|
||||
this.$imageMaxWidthHeight.on('change', () => {
|
||||
const opts = { 'imageMaxWidthHeight': this.$imageMaxWidthHeight.val() };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$imageJpegQuality.on('change', () => {
|
||||
const opts = { 'imageJpegQuality': this.$imageJpegQuality.val() };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$autoReadonlySizeText = $("#auto-readonly-size-text");
|
||||
|
||||
this.$autoReadonlySizeText.on('change', () => {
|
||||
const opts = { 'autoReadonlySizeText': this.$autoReadonlySizeText.val() };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$autoReadonlySizeCode = $("#auto-readonly-size-code");
|
||||
|
||||
this.$autoReadonlySizeCode.on('change', () => {
|
||||
const opts = { 'autoReadonlySizeCode': this.$autoReadonlySizeText.val() };
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$downloadImagesAutomatically = $("#download-images-automatically");
|
||||
|
||||
this.$downloadImagesAutomatically.on("change", () => {
|
||||
const isChecked = this.$downloadImagesAutomatically.prop("checked");
|
||||
const opts = { 'downloadImagesAutomatically': isChecked ? 'true' : 'false' };
|
||||
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
});
|
||||
|
||||
this.$enableImageCompression = $("#image-compresion-enabled");
|
||||
this.$imageCompressionWrapper = $("#image-compression-enabled-wraper");
|
||||
|
||||
this.setImageCompression = (isChecked) => {
|
||||
if (isChecked) {
|
||||
this.$imageCompressionWrapper.removeClass("disabled-field");
|
||||
} else {
|
||||
this.$imageCompressionWrapper.addClass("disabled-field");
|
||||
}
|
||||
};
|
||||
|
||||
this.$enableImageCompression.on("change", () => {
|
||||
const isChecked = this.$enableImageCompression.prop("checked");
|
||||
const opts = { 'compressImages': isChecked ? 'true' : 'false' };
|
||||
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
|
||||
this.setImageCompression(isChecked);
|
||||
});
|
||||
|
||||
this.$checkForUpdates = $("#check-for-updates");
|
||||
this.$checkForUpdates.on("change", () => {
|
||||
const isChecked = this.$checkForUpdates.prop("checked");
|
||||
const opts = { 'checkForUpdates': isChecked ? 'true' : 'false' };
|
||||
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
});
|
||||
this.$checkForUpdates = this.$widget.find("#check-for-updates");
|
||||
this.$checkForUpdates.on("change", () =>
|
||||
this.updateCheckboxOption('checkForUpdates', this.$checkForUpdates));
|
||||
}
|
||||
|
||||
optionsLoaded(options) {
|
||||
this.$spellCheckEnabled.prop("checked", options['spellCheckEnabled'] === 'true');
|
||||
this.$spellCheckLanguageCode.val(options['spellCheckLanguageCode']);
|
||||
this.$trayEnabled.prop("checked", options.disableTray !== 'true');
|
||||
|
||||
this.$eraseEntitiesAfterTimeInSeconds.val(options['eraseEntitiesAfterTimeInSeconds']);
|
||||
this.$protectedSessionTimeout.val(options['protectedSessionTimeout']);
|
||||
this.$noteRevisionsTimeInterval.val(options['noteRevisionSnapshotTimeInterval']);
|
||||
|
||||
this.$imageMaxWidthHeight.val(options['imageMaxWidthHeight']);
|
||||
this.$imageJpegQuality.val(options['imageJpegQuality']);
|
||||
|
||||
this.$autoReadonlySizeText.val(options['autoReadonlySizeText']);
|
||||
this.$autoReadonlySizeCode.val(options['autoReadonlySizeCode']);
|
||||
|
||||
const downloadImagesAutomatically = options['downloadImagesAutomatically'] === 'true';
|
||||
this.$downloadImagesAutomatically.prop('checked', downloadImagesAutomatically);
|
||||
|
||||
const compressImages = options['compressImages'] === 'true';
|
||||
this.$enableImageCompression.prop('checked', compressImages);
|
||||
this.setImageCompression(compressImages);
|
||||
|
||||
|
||||
const checkForUpdates = options['checkForUpdates'] === 'true';
|
||||
this.$checkForUpdates.prop('checked', checkForUpdates);
|
||||
this.$eraseEntitiesAfterTimeInSeconds.val(options.eraseEntitiesAfterTimeInSeconds);
|
||||
this.$noteRevisionsTimeInterval.val(options.noteRevisionSnapshotTimeInterval);
|
||||
|
||||
this.setCheckboxState(this.$checkForUpdates, options.checkForUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,63 @@
|
||||
import server from "../../../services/server.js";
|
||||
import protectedSessionHolder from "../../../services/protected_session_holder.js";
|
||||
import toastService from "../../../services/toast.js";
|
||||
import OptionsTab from "./options_tab.js";
|
||||
|
||||
const TPL = `
|
||||
<h3 id="password-heading"></h3>
|
||||
|
||||
<div class="alert alert-warning" role="alert" style="font-weight: bold; color: red !important;">
|
||||
Please take care to remember your new password. Password is used for logging into the web interface and
|
||||
to encrypt protected notes. If you forget your password, then all your protected notes are forever lost.
|
||||
In case you did forget your password, <a id="reset-password-button" href="javascript:">click here to reset it</a>.
|
||||
<div class="options-section">
|
||||
<h4 id="password-heading"></h4>
|
||||
|
||||
<div class="alert alert-warning" role="alert" style="font-weight: bold; color: red !important;">
|
||||
Please take care to remember your new password. Password is used for logging into the web interface and
|
||||
to encrypt protected notes. If you forget your password, then all your protected notes are forever lost.
|
||||
In case you did forget your password, <a id="reset-password-button" href="javascript:">click here to reset it</a>.
|
||||
</div>
|
||||
|
||||
<form id="change-password-form">
|
||||
<div class="form-group" id="old-password-form-group">
|
||||
<label for="old-password">Old password</label>
|
||||
<input class="form-control" id="old-password" type="password">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new-password1">New password</label>
|
||||
<input class="form-control" id="new-password1" type="password">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new-password2">New password Confirmation</label>
|
||||
<input class="form-control" id="new-password2" type="password">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" id="save-password-button">Change password</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form id="change-password-form">
|
||||
<div class="form-group" id="old-password-form-group">
|
||||
<label for="old-password">Old password</label>
|
||||
<input class="form-control" id="old-password" type="password">
|
||||
</div>
|
||||
<div class="options-section">
|
||||
<h4>Protected session timeout</h4>
|
||||
|
||||
<p>Protected session timeout is a time period after which the protected session is wiped from
|
||||
the browser's memory. This is measured from the last interaction with protected notes. See <a href="https://github.com/zadam/trilium/wiki/Protected-notes" class="external">wiki</a> for more info.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new-password1">New password</label>
|
||||
<input class="form-control" id="new-password1" type="password">
|
||||
<label for="protected-session-timeout-in-seconds">Protected session timeout (in seconds)</label>
|
||||
<input class="form-control" id="protected-session-timeout-in-seconds" type="number" min="60">
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new-password2">New password Confirmation</label>
|
||||
<input class="form-control" id="new-password2" type="password">
|
||||
</div>
|
||||
export default class PasswordOptions extends OptionsTab {
|
||||
get tabTitle() { return "Password" }
|
||||
|
||||
<button class="btn btn-primary" id="save-password-button">Change password</button>
|
||||
</form>`;
|
||||
lazyRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
export default class ChangePasswordOptions {
|
||||
constructor() {
|
||||
$("#options-password").html(TPL);
|
||||
|
||||
this.$passwordHeading = $("#password-heading");
|
||||
this.$form = $("#change-password-form");
|
||||
this.$oldPassword = $("#old-password");
|
||||
this.$newPassword1 = $("#new-password1");
|
||||
this.$newPassword2 = $("#new-password2");
|
||||
this.$savePasswordButton = $("#save-password-button");
|
||||
this.$resetPasswordButton = $("#reset-password-button");
|
||||
this.$passwordHeading = this.$widget.find("#password-heading");
|
||||
this.$changePasswordForm = this.$widget.find("#change-password-form");
|
||||
this.$oldPassword = this.$widget.find("#old-password");
|
||||
this.$newPassword1 = this.$widget.find("#new-password1");
|
||||
this.$newPassword2 = this.$widget.find("#new-password2");
|
||||
this.$savePasswordButton = this.$widget.find("#save-password-button");
|
||||
this.$resetPasswordButton = this.$widget.find("#reset-password-button");
|
||||
|
||||
this.$resetPasswordButton.on("click", async () => {
|
||||
if (confirm("By resetting the password you will forever lose access to all your existing protected notes. Do you really want to reset the password?")) {
|
||||
@@ -53,15 +70,20 @@ export default class ChangePasswordOptions {
|
||||
}
|
||||
});
|
||||
|
||||
this.$form.on('submit', () => this.save());
|
||||
this.$changePasswordForm.on('submit', () => this.save());
|
||||
|
||||
this.$protectedSessionTimeout = this.$widget.find("#protected-session-timeout-in-seconds");
|
||||
this.$protectedSessionTimeout.on('change', () =>
|
||||
this.updateOption('protectedSessionTimeout', this.$protectedSessionTimeout.val()));
|
||||
}
|
||||
|
||||
optionsLoaded(options) {
|
||||
const isPasswordSet = options.isPasswordSet === 'true';
|
||||
|
||||
$("#old-password-form-group").toggle(isPasswordSet);
|
||||
this.$widget.find("#old-password-form-group").toggle(isPasswordSet);
|
||||
this.$passwordHeading.text(isPasswordSet ? 'Change password' : 'Set password');
|
||||
this.$savePasswordButton.text(isPasswordSet ? 'Change password' : 'Set password');
|
||||
this.$protectedSessionTimeout.val(options.protectedSessionTimeout);
|
||||
}
|
||||
|
||||
save() {
|
||||
|
||||
@@ -1,46 +1,53 @@
|
||||
import server from "../../../services/server.js";
|
||||
import utils from "../../../services/utils.js";
|
||||
import dialogService from "../../dialog.js";
|
||||
import OptionsTab from "./options_tab.js";
|
||||
|
||||
const TPL = `
|
||||
<h4>Keyboard shortcuts</h4>
|
||||
|
||||
<p>Multiple shortcuts for the same action can be separated by comma.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" id="keyboard-shortcut-filter" placeholder="Type text to filter shortcuts...">
|
||||
</div>
|
||||
|
||||
<div style="overflow: auto; height: 500px;">
|
||||
<table id="keyboard-shortcut-table" cellpadding="10">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Action name</th>
|
||||
<th>Shortcuts</th>
|
||||
<th>Default shortcuts</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<button class="btn btn-primary" id="options-keyboard-shortcuts-reload-app">Reload app to apply changes</button>
|
||||
<div class="options-section">
|
||||
<h4>Keyboard shortcuts</h4>
|
||||
|
||||
<button class="btn" id="options-keyboard-shortcuts-set-all-to-default">Set all shortcuts to the default</button>
|
||||
</div>
|
||||
`;
|
||||
<p>
|
||||
Multiple shortcuts for the same action can be separated by comma.
|
||||
See <a href="https://www.electronjs.org/docs/latest/api/accelerator">Electron documentation</a> for available modifiers and key codes.
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" id="keyboard-shortcut-filter" placeholder="Type text to filter shortcuts...">
|
||||
</div>
|
||||
|
||||
<div style="overflow: auto; height: 500px;">
|
||||
<table id="keyboard-shortcut-table" cellpadding="10">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Action name</th>
|
||||
<th>Shortcuts</th>
|
||||
<th>Default shortcuts</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<button class="btn btn-primary" id="options-keyboard-shortcuts-reload-app">Reload app to apply changes</button>
|
||||
|
||||
<button class="btn" id="options-keyboard-shortcuts-set-all-to-default">Set all shortcuts to the default</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
let globActions;
|
||||
|
||||
export default class KeyboardShortcutsOptions {
|
||||
constructor() {
|
||||
$("#options-shortcuts").html(TPL);
|
||||
export default class KeyboardShortcutsOptions extends OptionsTab {
|
||||
get tabTitle() { return "Shortcuts" }
|
||||
|
||||
$("#options-keyboard-shortcuts-reload-app").on("click", () => utils.reloadFrontendApp());
|
||||
lazyRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
const $table = $("#keyboard-shortcut-table tbody");
|
||||
this.$widget.find("#options-keyboard-shortcuts-reload-app").on("click", () => utils.reloadFrontendApp());
|
||||
|
||||
const $table = this.$widget.find("#keyboard-shortcut-table tbody");
|
||||
|
||||
server.get('keyboard-actions').then(actions => {
|
||||
globActions = actions;
|
||||
@@ -73,7 +80,7 @@ export default class KeyboardShortcutsOptions {
|
||||
});
|
||||
|
||||
$table.on('change', 'input.form-control', e => {
|
||||
const $input = $(e.target);
|
||||
const $input = this.$widget.find(e.target);
|
||||
const actionName = $input.attr('data-keyboard-action-name');
|
||||
const shortcuts = $input.val()
|
||||
.replace('+,', "+Comma")
|
||||
@@ -81,54 +88,53 @@ export default class KeyboardShortcutsOptions {
|
||||
.map(shortcut => shortcut.replace("+Comma", "+,"))
|
||||
.filter(shortcut => !!shortcut);
|
||||
|
||||
const opts = {};
|
||||
opts['keyboardShortcuts' + actionName.substr(0, 1).toUpperCase() + actionName.substr(1)] = JSON.stringify(shortcuts);
|
||||
const optionName = 'keyboardShortcuts' + actionName.substr(0, 1).toUpperCase() + actionName.substr(1);
|
||||
|
||||
server.put('options', opts);
|
||||
this.updateOption(optionName, JSON.stringify(shortcuts));
|
||||
});
|
||||
|
||||
$("#options-keyboard-shortcuts-set-all-to-default").on('click', async () => {
|
||||
this.$widget.find("#options-keyboard-shortcuts-set-all-to-default").on('click', async () => {
|
||||
if (!await dialogService.confirm("Do you really want to reset all keyboard shortcuts to the default?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
$table.find('input.form-control').each(function() {
|
||||
const defaultShortcuts = $(this).attr('data-default-keyboard-shortcuts');
|
||||
const defaultShortcuts = this.$widget.find(this).attr('data-default-keyboard-shortcuts');
|
||||
|
||||
if ($(this).val() !== defaultShortcuts) {
|
||||
$(this)
|
||||
if (this.$widget.find(this).val() !== defaultShortcuts) {
|
||||
this.$widget.find(this)
|
||||
.val(defaultShortcuts)
|
||||
.trigger('change');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const $filter = $("#keyboard-shortcut-filter");
|
||||
const $filter = this.$widget.find("#keyboard-shortcut-filter");
|
||||
|
||||
$filter.on('keyup', () => {
|
||||
const filter = $filter.val().trim().toLowerCase();
|
||||
|
||||
$table.find("tr").each((i, el) => {
|
||||
if (!filter) {
|
||||
$(el).show();
|
||||
this.$widget.find(el).show();
|
||||
return;
|
||||
}
|
||||
|
||||
const actionName = $(el).find('input').attr('data-keyboard-action-name');
|
||||
const actionName = this.$widget.find(el).find('input').attr('data-keyboard-action-name');
|
||||
|
||||
if (!actionName) {
|
||||
$(el).hide();
|
||||
this.$widget.find(el).hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const action = globActions.find(act => act.actionName === actionName);
|
||||
|
||||
if (!action) {
|
||||
$(el).hide();
|
||||
this.$widget.find(el).hide();
|
||||
return;
|
||||
}
|
||||
|
||||
$(el).toggle(!!( // !! to avoid toggle overloads with different behavior
|
||||
this.$widget.find(el).toggle(!!( // !! to avoid toggle overloads with different behavior
|
||||
action.actionName.toLowerCase().includes(filter)
|
||||
|| action.defaultShortcuts.some(shortcut => shortcut.toLowerCase().includes(filter))
|
||||
|| action.effectiveShortcuts.some(shortcut => shortcut.toLowerCase().includes(filter))
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import utils from "../../../services/utils.js";
|
||||
import OptionsTab from "./options_tab.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="options-section">
|
||||
<h4>Spell check</h4>
|
||||
|
||||
<p>These options apply only for desktop builds, browsers will use their own native spell check. App restart is required after change.</p>
|
||||
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="spell-check-enabled">
|
||||
<label class="custom-control-label" for="spell-check-enabled">Enable spellcheck</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="spell-check-language-code">Language code(s)</label>
|
||||
<input type="text" class="form-control" id="spell-check-language-code" placeholder="for example "en-US", "de-AT"">
|
||||
</div>
|
||||
|
||||
<p>Multiple languages can be separated by comma, e.g. <code>en-US, de-DE, cs</code>. Changes to the spell check options will take effect after application restart.</p>
|
||||
|
||||
<p><strong>Available language codes: </strong> <span id="available-language-codes"></span></p>
|
||||
</div>`;
|
||||
|
||||
export default class SpellcheckOptions extends OptionsTab {
|
||||
get tabTitle() { return "Spellcheck" }
|
||||
|
||||
lazyRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$spellCheckEnabled = this.$widget.find("#spell-check-enabled");
|
||||
this.$spellCheckLanguageCode = this.$widget.find("#spell-check-language-code");
|
||||
|
||||
this.$spellCheckEnabled.on('change', () =>
|
||||
this.updateCheckboxOption('spellCheckEnabled', this.$spellCheckEnabled));
|
||||
|
||||
this.$spellCheckLanguageCode.on('change', () =>
|
||||
this.updateOption('spellCheckLanguageCode', this.$spellCheckLanguageCode.val()));
|
||||
|
||||
this.$availableLanguageCodes = this.$widget.find("#available-language-codes");
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const { webContents } = utils.dynamicRequire('@electron/remote').getCurrentWindow();
|
||||
|
||||
this.$availableLanguageCodes.text(webContents.session.availableSpellCheckerLanguages.join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
optionsLoaded(options) {
|
||||
this.setCheckboxState(this.$spellCheckEnabled, options.spellCheckEnabled);
|
||||
this.$spellCheckLanguageCode.val(options.spellCheckLanguageCode);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,56 @@
|
||||
import server from "../../../services/server.js";
|
||||
import toastService from "../../../services/toast.js";
|
||||
import OptionsTab from "./options_tab.js";
|
||||
|
||||
const TPL = `
|
||||
<h4 style="margin-top: 0px;">Sync configuration</h4>
|
||||
<div class="options-section">
|
||||
<h4 style="margin-top: 0px;">Sync configuration</h4>
|
||||
|
||||
<form id="sync-setup-form">
|
||||
<div class="form-group">
|
||||
<label for="sync-server-host">Server instance address</label>
|
||||
<input class="form-control" id="sync-server-host" placeholder="https://<host>:<port>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sync-server-timeout">Sync timeout (milliseconds)</label>
|
||||
<input class="form-control" id="sync-server-timeout" min="1" max="10000000" type="number" style="text-align: left;">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sync-proxy">Sync proxy server (optional)</label>
|
||||
<input class="form-control" id="sync-proxy" placeholder="https://<host>:<port>">
|
||||
|
||||
<p><strong>Note:</strong> If you leave the proxy setting blank, the system proxy will be used (applies to desktop/electron build only)</p>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<button class="btn btn-primary">Save</button>
|
||||
|
||||
<button class="btn" type="button" data-help-page="Synchronization">Help</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form id="sync-setup-form">
|
||||
<div class="form-group">
|
||||
<label for="sync-server-host">Server instance address</label>
|
||||
<input class="form-control" id="sync-server-host" placeholder="https://<host>:<port>">
|
||||
</div>
|
||||
<div class="options-section">
|
||||
<h4>Sync test</h4>
|
||||
|
||||
<p>This will test the connection and handshake to the sync server. If the sync server isn't initialized, this will set it up to sync with the local document.</p>
|
||||
|
||||
<button id="test-sync-button" class="btn">Test sync</button>
|
||||
</div>`;
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sync-server-timeout">Sync timeout (milliseconds)</label>
|
||||
<input class="form-control" id="sync-server-timeout" min="1" max="10000000" type="number" style="text-align: left;">
|
||||
</div>
|
||||
export default class SyncOptions extends OptionsTab {
|
||||
get tabTitle() { return "Sync" }
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sync-proxy">Sync proxy server (optional)</label>
|
||||
<input class="form-control" id="sync-proxy" placeholder="https://<host>:<port>">
|
||||
lazyRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
<p><strong>Note:</strong> If you leave the proxy setting blank, the system proxy will be used (applies to desktop/electron build only)</p>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<button class="btn btn-primary">Save</button>
|
||||
|
||||
<button class="btn" type="button" data-help-page="Synchronization">Help</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<br/>
|
||||
|
||||
<h4>Sync test</h4>
|
||||
|
||||
<p>This will test the connection and handshake to the sync server. If the sync server isn't initialized, this will set it up to sync with the local document.</p>
|
||||
|
||||
<button id="test-sync-button" class="btn">Test sync</button>`;
|
||||
|
||||
export default class SyncOptions {
|
||||
constructor() {
|
||||
$("#options-sync-setup").html(TPL);
|
||||
|
||||
this.$form = $("#sync-setup-form");
|
||||
this.$syncServerHost = $("#sync-server-host");
|
||||
this.$syncServerTimeout = $("#sync-server-timeout");
|
||||
this.$syncProxy = $("#sync-proxy");
|
||||
this.$testSyncButton = $("#test-sync-button");
|
||||
this.$form = this.$widget.find("#sync-setup-form");
|
||||
this.$syncServerHost = this.$widget.find("#sync-server-host");
|
||||
this.$syncServerTimeout = this.$widget.find("#sync-server-timeout");
|
||||
this.$syncProxy = this.$widget.find("#sync-proxy");
|
||||
this.$testSyncButton = this.$widget.find("#test-sync-button");
|
||||
|
||||
this.$form.on('submit', () => this.save());
|
||||
|
||||
@@ -62,19 +67,17 @@ export default class SyncOptions {
|
||||
}
|
||||
|
||||
optionsLoaded(options) {
|
||||
this.$syncServerHost.val(options['syncServerHost']);
|
||||
this.$syncServerTimeout.val(options['syncServerTimeout']);
|
||||
this.$syncProxy.val(options['syncProxy']);
|
||||
this.$syncServerHost.val(options.syncServerHost);
|
||||
this.$syncServerTimeout.val(options.syncServerTimeout);
|
||||
this.$syncProxy.val(options.syncProxy);
|
||||
}
|
||||
|
||||
save() {
|
||||
const opts = {
|
||||
this.updateMultipleOptions({
|
||||
'syncServerHost': this.$syncServerHost.val(),
|
||||
'syncServerTimeout': this.$syncServerTimeout.val(),
|
||||
'syncProxy': this.$syncProxy.val()
|
||||
};
|
||||
|
||||
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import server from "../../../services/server.js";
|
||||
import toastService from "../../../services/toast.js";
|
||||
import OptionsTab from "./options_tab.js";
|
||||
|
||||
const TPL = `
|
||||
<p><strong>Settings on this options tab are saved automatically after each change.</strong></p>
|
||||
|
||||
<form>
|
||||
<div class="options-section">
|
||||
<h4>Heading style</h4>
|
||||
<select class="form-control" id="heading-style">
|
||||
<option value="plain">Plain</option>
|
||||
<option value="underline">Underline</option>
|
||||
<option value="markdown">Markdown-style</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="options-section">
|
||||
<h4>Table of contents</h4>
|
||||
|
||||
Table of contents will appear in text notes when the note has more than a defined number of headings. You can customize this number:
|
||||
@@ -22,29 +24,42 @@ const TPL = `
|
||||
</div>
|
||||
|
||||
<p>You can also use this option to effectively disable TOC by setting a very high number.</p>
|
||||
</form>`;
|
||||
</div>
|
||||
|
||||
<div class="options-section">
|
||||
<h4>Automatic readonly size</h4>
|
||||
|
||||
export default class TextNotesOptions {
|
||||
constructor() {
|
||||
$("#options-text-notes").html(TPL);
|
||||
<p>Automatic readonly note size is the size after which notes will be displayed in a readonly mode (for performance reasons).</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="auto-readonly-size-text">Automatic readonly size (text notes)</label>
|
||||
<input class="form-control" id="auto-readonly-size-text" type="number" min="0" style="text-align: right;">
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class TextNotesOptions extends OptionsTab {
|
||||
get tabTitle() { return "Text notes" }
|
||||
|
||||
lazyRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$body = $("body");
|
||||
|
||||
this.$headingStyle = $("#heading-style");
|
||||
this.$headingStyle = this.$widget.find("#heading-style");
|
||||
this.$headingStyle.on('change', () => {
|
||||
const newHeadingStyle = this.$headingStyle.val();
|
||||
|
||||
this.toggleBodyClass("heading-style-", newHeadingStyle);
|
||||
|
||||
server.put('options/headingStyle/' + newHeadingStyle);
|
||||
this.updateOption('headingStyle', newHeadingStyle);
|
||||
});
|
||||
|
||||
this.$minTocHeadings = $("#min-toc-headings");
|
||||
this.$minTocHeadings.on('change', () => {
|
||||
const minTocHeadings = this.$minTocHeadings.val();
|
||||
this.$minTocHeadings = this.$widget.find("#min-toc-headings");
|
||||
this.$minTocHeadings.on('change', () =>
|
||||
this.updateOption('minTocHeadings', this.$minTocHeadings.val()));
|
||||
|
||||
server.put('options/minTocHeadings/' + minTocHeadings);
|
||||
});
|
||||
this.$autoReadonlySizeText = this.$widget.find("#auto-readonly-size-text");
|
||||
this.$autoReadonlySizeText.on('change', () =>
|
||||
this.updateOption('autoReadonlySizeText', this.$autoReadonlySizeText.val()));
|
||||
}
|
||||
|
||||
toggleBodyClass(prefix, value) {
|
||||
@@ -57,8 +72,9 @@ export default class TextNotesOptions {
|
||||
this.$body.addClass(prefix + value);
|
||||
}
|
||||
|
||||
async optionsLoaded(options) {
|
||||
optionsLoaded(options) {
|
||||
this.$headingStyle.val(options.headingStyle);
|
||||
this.$minTocHeadings.val(options.minTocHeadings);
|
||||
this.$autoReadonlySizeText.val(options.autoReadonlySizeText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export default class PasswordNoteSetDialog extends BasicWidget {
|
||||
this.$widget = $(TPL);
|
||||
this.$openPasswordOptionsButton = this.$widget.find(".open-password-options-button");
|
||||
this.$openPasswordOptionsButton.on("click", () => {
|
||||
this.triggerCommand("showOptions", { openTab: 'password' });
|
||||
this.triggerCommand("showOptions", { openTab: 'PasswordOptions' });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,19 @@ import appContext from "../../services/app_context.js";
|
||||
import hoistedNoteService from "../../services/hoisted_note.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import dialogService from "../dialog.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="recent-changes-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Recent changes</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<h5 class="modal-title mr-auto">Recent changes</h5>
|
||||
|
||||
<button class="erase-deleted-notes-now-button btn btn-xs" style="padding: 0 10px">
|
||||
Erase deleted notes now</button>
|
||||
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -29,20 +34,30 @@ export default class RecentChangesDialog extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$content = this.$widget.find(".recent-changes-content");
|
||||
this.$eraseDeletedNotesNow = this.$widget.find(".erase-deleted-notes-now-button");
|
||||
this.$eraseDeletedNotesNow.on("click", () => {
|
||||
server.post('notes/erase-deleted-notes-now').then(() => {
|
||||
this.refresh();
|
||||
|
||||
toastService.showMessage("Deleted notes have been erased.");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async showRecentChangesEvent({ancestorNoteId}) {
|
||||
await this.refresh(ancestorNoteId);
|
||||
this.ancestorNoteId = ancestorNoteId;
|
||||
|
||||
await this.refresh();
|
||||
|
||||
utils.openDialog(this.$widget);
|
||||
}
|
||||
|
||||
async refresh(ancestorNoteId) {
|
||||
if (!ancestorNoteId) {
|
||||
ancestorNoteId = hoistedNoteService.getHoistedNoteId();
|
||||
async refresh() {
|
||||
if (!this.ancestorNoteId) {
|
||||
this.ancestorNoteId = hoistedNoteService.getHoistedNoteId();
|
||||
}
|
||||
|
||||
const recentChangesRows = await server.get('recent-changes/' + ancestorNoteId);
|
||||
const recentChangesRows = await server.get('recent-changes/' + this.ancestorNoteId);
|
||||
|
||||
// preload all notes into cache
|
||||
await froca.getNotes(recentChangesRows.map(r => r.noteId), true);
|
||||
|
||||
@@ -283,11 +283,15 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
// probably incorrect event
|
||||
// calling this.refresh() is not enough since the event needs to be propagated to children as well
|
||||
// FIXME: create a separate event to force hierarchical refresh
|
||||
this.triggerEvent('noteTypeMimeChanged', {noteId: this.noteId});
|
||||
|
||||
// this uses handleEvent to make sure that the ordinary content updates are propagated only in the subtree
|
||||
// to avoid problem in #3365
|
||||
this.handleEvent('noteTypeMimeChanged', {noteId: this.noteId});
|
||||
}
|
||||
else if (loadResults.isNoteReloaded(this.noteId, this.componentId)
|
||||
&& (this.type !== await this.getWidgetType() || this.mime !== this.note.mime)) {
|
||||
|
||||
// this needs to have a triggerEvent so that e.g. note type (not in the component subtree) is updated
|
||||
this.triggerEvent('noteTypeMimeChanged', {noteId: this.noteId});
|
||||
}
|
||||
else {
|
||||
@@ -304,6 +308,8 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
&& attributeService.isAffecting(attr, this.note));
|
||||
|
||||
if (label || relation) {
|
||||
console.log("OOOO");
|
||||
|
||||
// probably incorrect event
|
||||
// calling this.refresh() is not enough since the event needs to be propagated to children as well
|
||||
this.triggerEvent('noteTypeMimeChanged', {noteId: this.noteId});
|
||||
|
||||
@@ -26,7 +26,7 @@ function loginSync(req) {
|
||||
|
||||
// login token is valid for 5 minutes
|
||||
if (Math.abs(timestamp.getTime() - now.getTime()) > 5 * 60 * 1000) {
|
||||
return [401, { message: 'Auth request time is out of sync, please check that both client and server have correct time.' }];
|
||||
return [401, { message: 'Auth request time is out of sync, please check that both client and server have correct time. The difference between clocks has to be smaller than 5 minutes.' }];
|
||||
}
|
||||
|
||||
const syncVersion = req.body.syncVersion;
|
||||
|
||||
@@ -317,7 +317,7 @@ function findExcerpts(sourceNote, referencedNoteId) {
|
||||
function getFilteredBacklinks(note) {
|
||||
return note.getTargetRelations()
|
||||
// search notes have "ancestor" relations which are not interesting
|
||||
.filter(note => note.getNote().type !== 'search');
|
||||
.filter(relation => !!relation.getNote() && relation.getNote().type !== 'search');
|
||||
}
|
||||
|
||||
function getBacklinkCount(req) {
|
||||
|
||||
@@ -59,7 +59,8 @@ const ALLOWED_OPTIONS = new Set([
|
||||
'compressImages',
|
||||
'downloadImagesAutomatically',
|
||||
'minTocHeadings',
|
||||
'checkForUpdates'
|
||||
'checkForUpdates',
|
||||
'disableTray'
|
||||
]);
|
||||
|
||||
function getOptions() {
|
||||
|
||||
+15
-8
@@ -197,17 +197,23 @@ function route(method, path, middleware, routeHandler, resultHandler, transactio
|
||||
const MAX_ALLOWED_FILE_SIZE_MB = 250;
|
||||
|
||||
const GET = 'get', POST = 'post', PUT = 'put', PATCH = 'patch', DELETE = 'delete';
|
||||
const uploadMiddleware = multer({
|
||||
|
||||
const multerOptions = {
|
||||
fileFilter: (req, file, cb) => {
|
||||
// UTF-8 file names are not well decoded by multer/busboy, so we handle the conversion on our side.
|
||||
// See https://github.com/expressjs/multer/pull/1102.
|
||||
file.originalname = Buffer.from(file.originalname, "latin1").toString("utf-8");
|
||||
cb(null, true);
|
||||
},
|
||||
limits: {
|
||||
fileSize: MAX_ALLOWED_FILE_SIZE_MB * 1024 * 1024
|
||||
}
|
||||
}).single('upload');
|
||||
};
|
||||
|
||||
if (!process.env.TRILIUM_NO_UPLOAD_LIMIT) {
|
||||
multerOptions.limits = {
|
||||
fileSize: MAX_ALLOWED_FILE_SIZE_MB * 1024 * 1024
|
||||
};
|
||||
}
|
||||
|
||||
const uploadMiddleware = multer(multerOptions).single('upload');
|
||||
|
||||
const uploadMiddlewareWithErrorHandling = function (req, res, next) {
|
||||
uploadMiddleware(req, res, function (err) {
|
||||
@@ -229,7 +235,8 @@ function register(app) {
|
||||
|
||||
const loginRateLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 10 // limit each IP to 10 requests per windowMs
|
||||
max: 10, // limit each IP to 10 requests per windowMs
|
||||
skipSuccessfulRequests: true // successful auth to rate-limited ETAPI routes isn't counted. However successful auth to /login is still counted!
|
||||
});
|
||||
|
||||
route(POST, '/login', [loginRateLimiter], loginRoute.login);
|
||||
@@ -384,7 +391,7 @@ function register(app) {
|
||||
apiRoute(GET, '/api/script/relation/:noteId/:relationName', scriptRoute.getRelationBundles);
|
||||
|
||||
// no CSRF since this is called from android app
|
||||
route(POST, '/api/sender/login', [], loginApiRoute.token, apiResultHandler);
|
||||
route(POST, '/api/sender/login', [loginRateLimiter], loginApiRoute.token, apiResultHandler);
|
||||
route(POST, '/api/sender/image', [auth.checkEtapiToken, uploadMiddlewareWithErrorHandling], senderRoute.uploadImage, apiResultHandler);
|
||||
route(POST, '/api/sender/note', [auth.checkEtapiToken], senderRoute.saveNote, apiResultHandler);
|
||||
|
||||
@@ -404,7 +411,7 @@ function register(app) {
|
||||
apiRoute(POST, '/api/login/protected/touch', loginApiRoute.touchProtectedSession);
|
||||
apiRoute(POST, '/api/logout/protected', loginApiRoute.logoutFromProtectedSession);
|
||||
|
||||
route(POST, '/api/login/token', [], loginApiRoute.token, apiResultHandler);
|
||||
route(POST, '/api/login/token', [loginRateLimiter], loginApiRoute.token, apiResultHandler);
|
||||
|
||||
// in case of local electron, local calls are allowed unauthenticated, for server they need auth
|
||||
const clipperMiddleware = utils.isElectron() ? [] : [auth.checkEtapiToken];
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2022-11-12T20:40:45+01:00", buildRevision: "3f115a7657e21470fc7a16bbc5d19d9aee89d825" };
|
||||
module.exports = { buildDate:"2022-12-02T22:44:40+01:00", buildRevision: "42cd33369407a1a64eeeced4a0729b5fcd7cac47" };
|
||||
|
||||
@@ -215,13 +215,13 @@ const DEFAULT_KEYBOARD_ACTIONS = [
|
||||
},
|
||||
{
|
||||
actionName: "activateNextTab",
|
||||
defaultShortcuts: isElectron ? ["CommandOrControl+Tab"] : [],
|
||||
defaultShortcuts: isElectron ? ["CommandOrControl+Tab", "CommandOrControl+PageDown"] : [],
|
||||
description: "Activates tab on the right",
|
||||
scope: "window"
|
||||
},
|
||||
{
|
||||
actionName: "activatePreviousTab",
|
||||
defaultShortcuts: isElectron ? ["CommandOrControl+Shift+Tab"] : [],
|
||||
defaultShortcuts: isElectron ? ["CommandOrControl+Shift+Tab", "CommandOrControl+PageUp"] : [],
|
||||
description: "Activates tab on the left",
|
||||
scope: "window"
|
||||
},
|
||||
|
||||
@@ -88,6 +88,7 @@ const defaultOptions = [
|
||||
{ name: 'downloadImagesAutomatically', value: 'true', isSynced: true },
|
||||
{ name: 'minTocHeadings', value: '5', isSynced: true },
|
||||
{ name: 'checkForUpdates', value: 'true', isSynced: true },
|
||||
{ name: 'disableTray', value: 'false', isSynced: false },
|
||||
];
|
||||
|
||||
function initStartupOptions() {
|
||||
|
||||
@@ -100,7 +100,7 @@ class NoteContentFulltextExp extends Expression {
|
||||
if (type === 'text' && mime === 'text/html') {
|
||||
if (!this.raw && content.length < 20000) { // striptags is slow for very large notes
|
||||
// allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412
|
||||
content = striptags(content, ['a']);
|
||||
content = striptags(content, ['a'], ' ');
|
||||
|
||||
// at least the closing tag can be easily stripped
|
||||
content = content.replace(/<\/a>/ig, "");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { Menu, Tray } = require('electron');
|
||||
const path = require('path');
|
||||
const windowService = require("./window");
|
||||
const optionService = require("./options");
|
||||
|
||||
const UPDATE_TRAY_EVENTS = [
|
||||
'minimize', 'maximize', 'show', 'hide'
|
||||
@@ -92,6 +93,10 @@ const changeVisibility = () => {
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
if (optionService.getOptionBool("disableTray")) {
|
||||
return;
|
||||
}
|
||||
|
||||
tray = new Tray(getIconPath());
|
||||
tray.setToolTip('Trilium Notes')
|
||||
// Restore focus
|
||||
|
||||
Reference in New Issue
Block a user