Compare commits

..

68 Commits

Author SHA1 Message Date
zadam 631a75deec release 0.39.2-beta 2019-12-30 19:48:54 +01:00
zadam 3f1d0e5872 small template refactoring 2019-12-30 19:46:48 +01:00
zadam 0fe91d0184 include note feature 2019-12-30 19:32:45 +01:00
zadam 2f711a12f8 added "include note" widget to text notes, WIP 2019-12-29 23:46:40 +01:00
zadam 64f32ba38f fix zen mode with new layout, closes #794 2019-12-29 20:48:24 +01:00
zadam 7db4859fb9 Merge remote-tracking branch 'origin/master' 2019-12-29 10:19:11 +01:00
zadam eee9fcae5c set timeout for the initial sync seed request 2019-12-29 10:19:00 +01:00
zadam 9c4a976342 added some statistic scripts to demo document 2019-12-28 21:52:35 +01:00
zadam e4a09c6207 fix left pane scrolling, closes #788 2019-12-28 21:37:13 +01:00
zadam d467db2227 unify API for creating note links 2019-12-28 21:10:02 +01:00
zadam b8d6ff0542 reset file inputs to allow reuploading the same file again 2019-12-28 19:29:52 +01:00
zadam a9b8e65c9b force scrolling when width of the content overflows 2019-12-28 19:18:44 +01:00
zadam bb8b563ece relaunch app after successful sync 2019-12-28 12:55:53 +01:00
zadam 05a8ffb944 small fixes 2019-12-28 10:28:12 +01:00
zadam 2502646a64 release 0.39.1-beta 2019-12-27 21:21:57 +01:00
zadam 3d95d69f80 styling hack for the design to look correct in both FF and chrome 2019-12-27 21:18:14 +01:00
zadam df751f5d67 fix icon 2019-12-27 21:11:56 +01:00
zadam 4f06b6de78 fix registration of global shortcuts, fixes #786 2019-12-27 20:28:27 +01:00
zadam d2177cd517 fix detection of desktop build in setup page, closes #787 2019-12-27 20:22:46 +01:00
zadam 0affcf5ad2 move electron-installer-debian to optional dependencies since it can't be installed on windows, #783 2019-12-26 10:02:55 +01:00
zadam 7a416b107b fix electron dep issue in the server version, closes #784 2019-12-25 19:38:28 +01:00
zadam 1ff124dab7 release 0.39.0-beta 2019-12-25 12:18:52 +01:00
zadam 4cb511bad0 fix import with style.css 2019-12-25 12:09:49 +01:00
zadam 73c8d145fa tar export fixes + added code block example to the demo document 2019-12-25 11:56:55 +01:00
zadam ab79f24729 added content style to tar html export as well 2019-12-25 11:34:45 +01:00
zadam cec71f65b3 load ckeditor content style for printing to have more similar look to the in-editor, #782 2019-12-25 10:59:45 +01:00
zadam f75c008154 more sync recovery fixes 2019-12-24 17:49:44 +01:00
zadam 474baa7d95 sync recovery fixes 2019-12-24 16:00:31 +01:00
zadam a155b6e8d5 create separate window for setup and then main window 2019-12-24 14:42:03 +01:00
zadam 229974e543 added option to enable native title bar (disabled by default) 2019-12-24 12:10:32 +01:00
zadam 6fc19bfb93 Merge branch 'stable' 2019-12-24 11:55:27 +01:00
zadam ccaa108faa release 0.38.3 2019-12-24 10:51:51 +01:00
zadam 0a72383495 make opening new links from ckeditor more consistent for internal and external links, closes #779 2019-12-24 10:49:16 +01:00
zadam d389100611 properly cleanup sidebar content after closing tab 2019-12-23 21:46:37 +01:00
zadam ea7257a5b2 cleaned up experimental attribute pane 2019-12-23 21:13:56 +01:00
zadam 0ebc947fbd visual tweaks 2019-12-23 21:05:47 +01:00
zadam c89514f9bb saving size and visibility state of the panes 2019-12-23 20:34:29 +01:00
zadam e0368e395c removed left and right sidebar sizing options 2019-12-23 19:45:59 +01:00
zadam 6986c201dd removed hideTabRowForOneTab option 2019-12-23 19:39:48 +01:00
zadam bcf163f8a1 css alignment 2019-12-23 17:12:17 +01:00
zadam 15aaead7b9 show/hide switcher for the sidebar 2019-12-23 16:48:34 +01:00
zadam d29c5c4758 sidebar pulled outside of the tab content and added splitter 2019-12-23 15:50:24 +01:00
zadam 81e2baeee5 blur title buttons after clicking 2019-12-23 13:46:26 +01:00
zadam 4cececafc9 added title bar buttons 2019-12-23 13:34:54 +01:00
zadam 7c8e7a3f4b frameless design with tabs on top, split for left panel 2019-12-23 11:52:45 +01:00
zadam 613d5f93e8 convert css grid design to flex based one 2019-12-23 08:52:57 +01:00
zadam 4f5b23fbf8 tar file export now sets mtime to tar records based on utcDateCreated of a note, closes #487 2019-12-22 10:57:55 +01:00
zadam a37b9cfc7b steel blue tweaks in demo document 2019-12-21 15:48:36 +01:00
zadam 2bc18bc214 use main border color for tab border 2019-12-21 15:37:51 +01:00
zadam f31a998c5d path list contains a button to add a new path, closes #611 2019-12-21 13:55:13 +01:00
zadam 5552917533 Merge branch 'stable' 2019-12-21 13:39:12 +01:00
zadam a9702aa6a2 fix empty checkbox visibility, closes #775 2019-12-21 12:37:24 +01:00
zadam d1941cc650 Merge branch 'stable' 2019-12-20 21:00:30 +01:00
zadam f98fa4098f clearer WS error message 2019-12-20 20:17:58 +01:00
zadam 5350496ed4 fix creating note from global ctrl+alt+p shortcut, closes #773 2019-12-20 20:13:21 +01:00
zadam b62d79044a fix creating note from relation map, closes #771 2019-12-20 19:02:52 +01:00
zadam 0db3722ec2 package updates 2019-12-20 18:04:05 +01:00
zadam d47403c0e7 implemented sync hash check recovery process 2019-12-18 22:58:30 +01:00
zadam 77311954a1 added sectors for contect check computation 2019-12-18 22:24:13 +01:00
zadam b7cf4fe96b more general filter parsing 2019-12-18 20:51:48 +01:00
zadam 6d9b702d4c Merge branch 'master' into stable 2019-12-18 20:49:24 +01:00
zadam 6e4c30571c release 0.38.2 2019-12-18 20:21:06 +01:00
zadam 5988776b7e styling of active button 2019-12-18 20:16:11 +01:00
zadam 384da60953 fix regex for parsing out the filters 2019-12-18 19:56:53 +01:00
zadam 21fab412cb sync error mitigation 2019-12-17 22:17:03 +01:00
zadam eb4dfbad92 sync fixes 2019-12-16 22:47:07 +01:00
zadam aff9ce97ee small sync fixes 2019-12-16 22:00:44 +01:00
zadam 882ebdbd8f revert unicode regex since it's still not supported by ff 2019-12-09 23:08:32 +01:00
71 changed files with 1842 additions and 994 deletions
+2 -2
View File
@@ -2,7 +2,7 @@
<dataSource name="document.db">
<database-model serializer="dbm" dbms="SQLITE" family-id="SQLITE" format-version="4.17">
<root id="1">
<ServerVersion>3.25.1</ServerVersion>
<ServerVersion>3.16.1</ServerVersion>
</root>
<schema id="2" parent="1" name="main">
<Current>1</Current>
@@ -587,7 +587,7 @@ parentNoteId</ColNames>
</column>
<column id="134" parent="16" name="rootpage">
<Position>4</Position>
<DataType>int|0s</DataType>
<DataType>integer|0s</DataType>
</column>
<column id="135" parent="16" name="sql">
<Position>5</Position>
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:12.13.1-alpine
FROM node:12.14.0-alpine
# Create app directory
WORKDIR /usr/src/app
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
PKG_DIR=dist/trilium-linux-x64-server
NODE_VERSION=12.13.1
NODE_VERSION=12.14.0
if [ "$1" != "DONTCOPY" ]
then
BIN
View File
Binary file not shown.
+15 -111
View File
@@ -1,86 +1,17 @@
'use strict';
const {app, globalShortcut, BrowserWindow} = require('electron');
const path = require('path');
const log = require('./src/services/log');
const {app, globalShortcut} = require('electron');
const sqlInit = require('./src/services/sql_init');
const cls = require('./src/services/cls');
const url = require("url");
const port = require('./src/services/port');
const env = require('./src/services/env');
const keyboardActionsService = require('./src/services/keyboard_actions');
const appIconService = require('./src/services/app_icon');
const windowStateKeeper = require('electron-window-state');
const windowService = require('./src/services/window');
// Adds debug features like hotkeys for triggering dev tools and reload
require('electron-debug')();
appIconService.installLocalAppIcon();
// Prevent window being garbage collected
let mainWindow;
require('electron-dl')({ saveAs: true });
function onClosed() {
// Dereference the window
// For multiple windows store them in an array
mainWindow = null;
}
async function createMainWindow() {
await sqlInit.dbConnection;
// if schema doesn't exist -> setup process
// if schema exists, then we need to wait until the migration process is finished
if (await sqlInit.schemaExists()) {
await sqlInit.dbReady;
}
const mainWindowState = windowStateKeeper({
// default window width & height so it's usable on 1600 * 900 display (including some extra panels etc.)
defaultWidth: 1200,
defaultHeight: 800
});
const win = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
title: 'Trilium Notes',
webPreferences: {
nodeIntegration: true
},
icon: path.join(__dirname, 'images/app-icons/png/256x256' + (env.isDev() ? '-dev' : '') + '.png')
});
mainWindowState.manage(win);
win.setMenuBarVisibility(false);
win.loadURL('http://127.0.0.1:' + await port);
win.on('closed', onClosed);
win.webContents.on('new-window', (e, url) => {
if (url !== win.webContents.getURL()) {
e.preventDefault();
require('electron').shell.openExternal(url);
}
});
// prevent drag & drop to navigate away from trilium
win.webContents.on('will-navigate', (ev, targetUrl) => {
const parsedUrl = url.parse(targetUrl);
// we still need to allow internal redirects from setup and migration pages
if (!['localhost', '127.0.0.1'].includes(parsedUrl.hostname) || (parsedUrl.path && parsedUrl.path !== '/')) {
ev.preventDefault();
}
});
return win;
}
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
@@ -90,50 +21,23 @@ app.on('window-all-closed', () => {
}
});
app.on('activate', () => {
if (!mainWindow) {
mainWindow = createMainWindow();
}
});
async function registerGlobalShortcuts() {
await sqlInit.dbReady;
const allActions = await keyboardActionsService.getKeyboardActions();
for (const action of allActions) {
if (!action.effectiveShortcuts) {
continue;
}
for (const shortcut of action.effectiveShortcuts) {
if (shortcut.startsWith('global:')) {
const translatedShortcut = shortcut.substr(7);
const result = globalShortcut.register(translatedShortcut, cls.wrap(async () => {
// window may be hidden / not in focus
mainWindow.focus();
mainWindow.webContents.send('globalShortcut', action.actionName);
}));
if (result) {
log.info(`Registered global shortcut ${translatedShortcut} for action ${action.actionName}`);
}
else {
log.info(`Could not register global shortcut ${translatedShortcut}`);
}
}
}
}
}
app.on('ready', async () => {
app.setAppUserModelId('com.github.zadam.trilium');
mainWindow = await createMainWindow();
await sqlInit.dbConnection;
registerGlobalShortcuts();
// if schema doesn't exist -> setup process
// if schema exists, then we need to wait until the migration process is finished
if (await sqlInit.schemaExists()) {
await sqlInit.dbReady;
await windowService.createMainWindow();
}
else {
await windowService.createSetupWindow();
}
await windowService.registerGlobalShortcuts();
});
app.on('will-quit', () => {
+310
View File
@@ -0,0 +1,310 @@
/*
* !!!!!!! This stylesheet is heavily modified compared to the original for similarity with in-editor look !!!!!!!
* This is used for printing and tar HTML export
*
* CKEditor 5 (v15.0.0) content styles.
* Generated on Wed, 27 Nov 2019 13:26:13 GMT.
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html
*/
:root {
--ck-highlight-marker-blue: #72cdfd;
--ck-highlight-marker-green: #63f963;
--ck-highlight-marker-pink: #fc7999;
--ck-highlight-marker-yellow: #fdfd77;
--ck-highlight-pen-green: #118800;
--ck-highlight-pen-red: #e91313;
--ck-image-style-spacing: 1.5em;
--ck-todo-list-checkmark-size: 16px;
font-family: Arial, Sans-Serif;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list {
list-style: none;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list li {
margin-bottom: 5px;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list li .todo-list {
margin-top: 5px;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label > input {
-webkit-appearance: none;
display: inline-block;
position: relative;
width: var(--ck-todo-list-checkmark-size);
height: var(--ck-todo-list-checkmark-size);
vertical-align: middle;
border: 0;
left: -25px;
margin-right: -15px;
right: 0;
margin-left: 0;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label > input::before {
display: block;
position: absolute;
box-sizing: border-box;
content: '';
width: 100%;
height: 100%;
border: 1px solid hsl(0, 0%, 20%);
border-radius: 2px;
transition: 250ms ease-in-out box-shadow, 250ms ease-in-out background, 250ms ease-in-out border;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label > input::after {
display: block;
position: absolute;
box-sizing: content-box;
pointer-events: none;
content: '';
left: calc( var(--ck-todo-list-checkmark-size) / 3 );
top: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
width: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
height: calc( var(--ck-todo-list-checkmark-size) / 2.6 );
border-style: solid;
border-color: transparent;
border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0;
transform: rotate(45deg);
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label > input[checked]::before {
border-color: hsl(126, 64%, 41%);
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label > input[checked]::after {
border-color: hsl(126, 64%, 41%);
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label .todo-list__label__description {
vertical-align: middle;
}
/* ckeditor5-image/theme/image.css */
.image {
display: table;
clear: both;
text-align: center;
margin: 1em auto;
}
/* ckeditor5-image/theme/image.css */
.image > img {
display: block;
margin: 0 auto;
max-width: 100%;
min-width: 50px;
}
/* ckeditor5-block-quote/theme/blockquote.css */
blockquote {
overflow: hidden;
padding-right: 1.5em;
padding-left: 1.5em;
margin-left: 0;
margin-right: 0;
font-style: italic;
border-left: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-block-quote/theme/blockquote.css */
.ck-content[dir="rtl"] blockquote {
border-left: 0;
border-right: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-image/theme/imageresize.css */
.image.image_resized {
max-width: 100%;
display: block;
box-sizing: border-box;
}
/* ckeditor5-image/theme/imageresize.css */
.image.image_resized img {
width: 100%;
}
/* ckeditor5-image/theme/imageresize.css */
.image.image_resized > figcaption {
display: block;
}
/* ckeditor5-image/theme/imagestyle.css */
.image-style-side,
.image-style-align-left,
.image-style-align-center,
.image-style-align-right {
max-width: 50%;
}
/* ckeditor5-image/theme/imagestyle.css */
.image-style-side {
float: right;
margin-left: var(--ck-image-style-spacing);
}
/* ckeditor5-image/theme/imagestyle.css */
.image-style-align-left {
float: left;
margin-right: var(--ck-image-style-spacing);
}
/* ckeditor5-image/theme/imagestyle.css */
.image-style-align-center {
margin-left: auto;
margin-right: auto;
}
/* ckeditor5-image/theme/imagestyle.css */
.image-style-align-right {
float: right;
margin-left: var(--ck-image-style-spacing);
}
/* ckeditor5-media-embed/theme/mediaembed.css */
.media {
clear: both;
margin: 1em 0;
display: block;
min-width: 15em;
}
/* ckeditor5-table/theme/table.css */
.table {
display: table;
margin: 0;
}
/* ckeditor5-table/theme/table.css */
.table table {
border-collapse: collapse;
border-spacing: 0;
border: 1px double hsl(0, 0%, 70%);
}
/* ckeditor5-table/theme/table.css */
.table table td,
.table table th {
min-width: 2em;
padding: .4em;
border: 1px solid #d9d9d9;
}
/* ckeditor5-table/theme/table.css */
.table table th {
font-weight: bold;
background-color: #f5f5f5;
text-align: left;
}
/* ckeditor5-highlight/theme/highlight.css */
.marker-yellow {
background-color: var(--ck-highlight-marker-yellow);
}
/* ckeditor5-highlight/theme/highlight.css */
.marker-green {
background-color: var(--ck-highlight-marker-green);
}
/* ckeditor5-highlight/theme/highlight.css */
.marker-pink {
background-color: var(--ck-highlight-marker-pink);
}
/* ckeditor5-highlight/theme/highlight.css */
.marker-blue {
background-color: var(--ck-highlight-marker-blue);
}
/* ckeditor5-highlight/theme/highlight.css */
.pen-red {
color: var(--ck-highlight-pen-red);
background-color: transparent;
}
/* ckeditor5-highlight/theme/highlight.css */
.pen-green {
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.page-break {
position: relative;
clear: both;
padding: 5px 0;
display: flex;
align-items: center;
justify-content: center;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.page-break::after {
content: '';
position: absolute;
border-bottom: 2px dashed hsl(0, 0%, 77%);
width: 100%;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.page-break__label {
position: relative;
z-index: 1;
padding: .3em .6em;
display: block;
text-transform: uppercase;
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
font-size: 0.75em;
font-weight: bold;
color: hsl(0, 0%, 20%);
background: #fff;
box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* ckeditor5-basic-styles/theme/code.css */
code {
background-color: hsla(0, 0%, 78%, 0.3);
padding: .15em;
border-radius: 2px;
}
/* ckeditor5-image/theme/imagecaption.css */
.image > figcaption {
display: table-caption;
caption-side: bottom;
word-break: break-word;
color: hsl(0, 0%, 20%);
background-color: hsl(0, 0%, 97%);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* ckeditor5-horizontal-line/theme/horizontalline.css */
hr {
border-width: 1px 0 0;
border-style: solid;
border-color: hsl(0, 0%, 37%);
margin: 0;
}
/* ckeditor5-code-block/theme/codeblock.css */
pre {
padding: 1em;
color: #353535;
background: hsla(0, 0%, 78%, 0.3);
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
text-align: left;
direction: ltr;
tab-size: 4;
white-space: pre-wrap;
font-style: normal;
min-width: 200px;
}
/* ckeditor5-code-block/theme/codeblock.css */
pre code {
background: unset;
padding: 0;
border-radius: 0;
}
.ck-widget__selection-handle {
display: none;
}
@media print {
/* ckeditor5-page-break/theme/pagebreak.css */
.page-break {
padding: 0;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.page-break::after {
display: none;
}
}
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
View File
File diff suppressed because one or more lines are too long
+149 -153
View File
@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.38.0-beta",
"version": "0.39.0-beta",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -629,12 +629,9 @@
"integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA=="
},
"agent-base": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
"integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
"requires": {
"es6-promisify": "^5.0.0"
}
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz",
"integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g=="
},
"ajv": {
"version": "6.10.2",
@@ -760,6 +757,12 @@
"temp-file": "^3.3.4"
},
"dependencies": {
"ejs": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz",
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==",
"dev": true
},
"hosted-git-info": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.2.tgz",
@@ -768,6 +771,12 @@
"requires": {
"lru-cache": "^5.1.1"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
@@ -871,7 +880,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/asar/-/asar-2.0.1.tgz",
"integrity": "sha512-Vo9yTuUtyFahkVMFaI6uMuX6N7k5DWa6a/8+7ov0/f8Lq9TVR0tUjzSzxQSxT1Y+RJIZgnP7BVb6Uhi+9cjxqA==",
"dev": true,
"requires": {
"chromium-pickle-js": "^0.2.0",
"commander": "^2.20.0",
@@ -885,14 +893,12 @@
"commander": {
"version": "2.20.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz",
"integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==",
"dev": true
"integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg=="
},
"glob": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
"integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -906,7 +912,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-1.1.0.tgz",
"integrity": "sha512-8+Ah9aB1IRXCnIOxXZ0uFozV1nMU5xiu7hhFVUSxZ3bYu+psD4TzagCzVbexUCgNNGJnsmNDQlS4nG3mTyoNkw==",
"dev": true,
"requires": {
"bluebird": "^3.5.0",
"tmp": "0.1.0"
@@ -972,8 +977,7 @@
"author-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/author-regex/-/author-regex-1.0.0.tgz",
"integrity": "sha1-0IiFvmubv5Q5/gh8dihyRfCoFFA=",
"dev": true
"integrity": "sha1-0IiFvmubv5Q5/gh8dihyRfCoFFA="
},
"aws-sign2": {
"version": "0.7.0",
@@ -1137,8 +1141,7 @@
"bluebird": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.0.tgz",
"integrity": "sha512-aBQ1FxIa7kSWCcmKHlcHFlT2jt6J/l4FzC7KcPELkOJOsPOb/bccdhmIrKDfXhwFrmc7vDoDrrepFvGqjyXGJg==",
"dev": true
"integrity": "sha512-aBQ1FxIa7kSWCcmKHlcHFlT2jt6J/l4FzC7KcPELkOJOsPOb/bccdhmIrKDfXhwFrmc7vDoDrrepFvGqjyXGJg=="
},
"bluebird-lst": {
"version": "1.0.9",
@@ -1592,8 +1595,7 @@
"chromium-pickle-js": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz",
"integrity": "sha1-BKEGZywYsIWrd02YPfo+oTjyIgU=",
"dev": true
"integrity": "sha1-BKEGZywYsIWrd02YPfo+oTjyIgU="
},
"ci-info": {
"version": "2.0.0",
@@ -1662,7 +1664,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"dev": true,
"requires": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
@@ -1672,20 +1673,17 @@
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
@@ -1696,7 +1694,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
@@ -1752,7 +1749,6 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
@@ -1760,8 +1756,7 @@
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"color-support": {
"version": "1.1.3",
@@ -1977,7 +1972,7 @@
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/cross-spawn-promise/-/cross-spawn-promise-0.10.1.tgz",
"integrity": "sha1-25y0xQxgtyoVvgSbeBIs44LYexA=",
"dev": true,
"optional": true,
"requires": {
"cross-spawn": "^5.1.0"
}
@@ -2050,8 +2045,7 @@
"cuint": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz",
"integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=",
"dev": true
"integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs="
},
"currently-unhandled": {
"version": "0.4.1",
@@ -2102,9 +2096,9 @@
"integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI="
},
"dayjs": {
"version": "1.8.17",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.17.tgz",
"integrity": "sha512-47VY/htqYqr9GHd7HW/h56PpQzRBSJcxIQFwqL3P20bMF/3az5c3PWdVY3LmPXFl6cQCYHL7c79b9ov+2bOBbw=="
"version": "1.8.18",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.18.tgz",
"integrity": "sha512-JBMJZghNK8TtuoPnKNIzW9xavVVigld/zmZNpZSyQbkb2Opp55YIfZUpE4OEqPF/iyUVQTKcn1bC2HtC8B7s3g=="
},
"debug": {
"version": "4.1.1",
@@ -2473,6 +2467,20 @@
"sanitize-filename": "^1.6.2",
"semver": "^6.3.0",
"temp-file": "^3.3.4"
},
"dependencies": {
"ejs": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz",
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==",
"dev": true
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
"builder-util": {
@@ -2732,9 +2740,9 @@
},
"dependencies": {
"@types/node": {
"version": "10.17.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.0.tgz",
"integrity": "sha512-wuJwN2KV4tIRz1bu9vq5kSPasJ8IsEjZaP1ZR7KlmdUZvGF/rXy8DmXOVwUD0kAtvtJ7aqMKPqUXC0NUTDbrDg==",
"version": "10.17.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.11.tgz",
"integrity": "sha512-dNd2pp8qTzzNLAs3O8nH3iU9DG9866KHq9L3ISPB7DOGERZN81nW/5/g/KzMJpCU8jrbCiMRBzV9/sCEdRosig==",
"dev": true
}
}
@@ -2877,13 +2885,13 @@
}
},
"electron-dl": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-1.14.0.tgz",
"integrity": "sha512-4okyei42a1mLsvLK7hLrIfd20EQzB18nIlLTwBV992aMSmTGLUEFRTmO1MfSslGNrzD8nuPuy1l/VxO8so4lig==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-2.0.0.tgz",
"integrity": "sha512-crv64+Z2ZCsHCoJHBBOypDKx8cy+4jTGMc5/rDDzIajihDN/wITkJOdb6XG8b43YlWr3NS1/r6j+IdwaFNmMiQ==",
"requires": {
"ext-name": "^5.0.0",
"pupa": "^1.0.0",
"unused-filename": "^1.0.0"
"pupa": "^2.0.1",
"unused-filename": "^2.1.0"
}
},
"electron-download": {
@@ -2946,7 +2954,7 @@
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/electron-installer-common/-/electron-installer-common-0.7.3.tgz",
"integrity": "sha512-l4chYFTWr6uWODKYUXeC+Z4tqGa3b8e+Y2WUBf3F7Ruv6yYzZ+Ccic65oXreeotx09B7sUx1KTuwXRsRJHKlMw==",
"dev": true,
"optional": true,
"requires": {
"asar": "^2.0.1",
"cross-spawn-promise": "^0.10.1",
@@ -2963,7 +2971,7 @@
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
"integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==",
"dev": true,
"optional": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -2972,6 +2980,12 @@
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"optional": true
}
}
},
@@ -2979,7 +2993,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/electron-installer-debian/-/electron-installer-debian-2.0.1.tgz",
"integrity": "sha512-6/rgdWoezE5/JKSu6Y1mK3TT2lUeRKpviVoh063DKSLwcDPHQFgJ0/uDxqGppi3AF4du8n1vVio0VEkCEwlM1A==",
"dev": true,
"optional": true,
"requires": {
"debug": "^4.1.1",
"electron-installer-common": "^0.7.1",
@@ -2994,19 +3008,19 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
"optional": true
},
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true
"optional": true
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dev": true,
"optional": true,
"requires": {
"locate-path": "^3.0.0"
}
@@ -3015,13 +3029,13 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
"optional": true
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"optional": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
@@ -3032,7 +3046,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^4.1.0"
}
@@ -3041,7 +3055,7 @@
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.0.tgz",
"integrity": "sha512-/is78VKbKs70bVZH7w4YaZea6xcJWOAwkhbR0CFuZBmYtfTYF0xjGJF43AYd8g2Uii1yJwmS5GR2vBmrc32sbg==",
"dev": true,
"optional": true,
"requires": {
"cliui": "^5.0.0",
"decamelize": "^1.2.0",
@@ -3060,7 +3074,7 @@
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.0.tgz",
"integrity": "sha512-xLTUnCMc4JhxrPEPUYD5IBR1mWCK/aT6+RJ/K29JY2y1vD+FhtgKK0AXRWvI262q3QSffAQuTouFIKUuHX89wQ==",
"dev": true,
"optional": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
@@ -3166,6 +3180,14 @@
"sanitize-filename": "^1.6.0",
"semver": "^6.0.0",
"yargs-parser": "^16.0.0"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
"electron-publish": {
@@ -3320,8 +3342,7 @@
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
"dev": true
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
},
"encodeurl": {
"version": "1.0.2",
@@ -3389,18 +3410,10 @@
"dev": true,
"optional": true
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"es6-promisify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"requires": {
"es6-promise": "^4.0.3"
}
"escape-goat": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
"integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q=="
},
"escape-html": {
"version": "1.0.3",
@@ -3834,9 +3847,9 @@
}
},
"file-type": {
"version": "12.4.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-12.4.0.tgz",
"integrity": "sha512-WTvyKq8yjtNmUtVAD8LGcTkvtCdJglM6ks2HTqEClm6+65XTqM6MoZYA1Vtra50DLRWLiM38fEs1y56f5VhnUA=="
"version": "12.4.2",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-12.4.2.tgz",
"integrity": "sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg=="
},
"filename-regex": {
"version": "2.0.1",
@@ -4094,7 +4107,7 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/gar/-/gar-1.0.4.tgz",
"integrity": "sha512-w4n9cPWyP7aHxKxYHFQMegj7WIAsL/YX/C4Bs5Rr8s1H9M1rNtRWRsw+ovYMkXDQ5S4ZbYHsHAPmevPjPgw44w==",
"dev": true
"optional": true
},
"gauge": {
"version": "2.7.4",
@@ -4121,14 +4134,13 @@
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
},
"get-folder-size": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/get-folder-size/-/get-folder-size-2.0.1.tgz",
"integrity": "sha512-+CEb+GDCM7tkOS2wdMKTn9vU7DgnKUTuDlehkNJKNSovdCOVxs14OfKCk4cvSaR3za4gj+OBdl9opPN9xrJ0zA==",
"dev": true,
"optional": true,
"requires": {
"gar": "^1.0.4",
"tiny-each-async": "2.0.3"
@@ -4452,6 +4464,13 @@
"integrity": "sha512-vKea1DrcLA80Hrfc7AQgfoGvEaByfR5mH08t+zuWOWY94TFBmabdEL56mUbcijvadG9RxsXR2gUUFrfj4/iTcA==",
"dev": true,
"optional": true
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true,
"optional": true
}
}
},
@@ -4896,22 +4915,12 @@
}
},
"http-proxy-agent": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz",
"integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-3.0.0.tgz",
"integrity": "sha512-uGuJaBWQWDQCJI5ip0d/VTYZW0nRrlLWXA4A7P1jrsa+f77rW2yXz315oBt6zGCF6l8C2tlMxY7ffULCj+5FhA==",
"requires": {
"agent-base": "4",
"debug": "3.1.0"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
}
"agent-base": "5",
"debug": "4"
}
},
"http-signature": {
@@ -4925,27 +4934,12 @@
}
},
"https-proxy-agent": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz",
"integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz",
"integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==",
"requires": {
"agent-base": "^4.3.0",
"debug": "^3.1.0"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
"agent-base": "5",
"debug": "4"
}
},
"iconv-lite": {
@@ -5819,7 +5813,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dev": true,
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
@@ -6068,6 +6061,13 @@
"integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==",
"requires": {
"semver": "^6.0.0"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
}
}
},
"map-obj": {
@@ -7399,7 +7399,6 @@
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz",
"integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
}
@@ -7408,7 +7407,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dev": true,
"requires": {
"p-limit": "^2.0.0"
}
@@ -7442,8 +7440,7 @@
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
},
"package-json": {
"version": "6.5.0",
@@ -7547,6 +7544,12 @@
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
"dev": true
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
},
"url-parse-lax": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
@@ -7567,7 +7570,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/parse-author/-/parse-author-2.0.0.tgz",
"integrity": "sha1-00YL8d3Q367tQtp1QkLmX7aEqB8=",
"dev": true,
"requires": {
"author-regex": "^1.0.0"
}
@@ -8496,9 +8498,12 @@
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
"pupa": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/pupa/-/pupa-1.0.0.tgz",
"integrity": "sha1-mpVopa9+ZXuEYqbp1TKHQ1YM7/Y="
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz",
"integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==",
"requires": {
"escape-goat": "^2.0.0"
}
},
"qs": {
"version": "6.7.0",
@@ -8796,14 +8801,12 @@
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
"dev": true
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"requizzle": {
"version": "0.2.3",
@@ -8966,9 +8969,9 @@
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.1.1.tgz",
"integrity": "sha512-WfuG+fl6eh3eZ2qAf6goB7nhiCd7NPXhmyFxigB/TOkQyeLP8w8GsVehvtGNtnNmyboz4TgeK40B1Kbql/8c5A=="
},
"semver-compare": {
"version": "1.0.0",
@@ -9706,7 +9709,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tiny-each-async/-/tiny-each-async-2.0.3.tgz",
"integrity": "sha1-jru/1tYpXxNwAD+7NxYq/loKUdE=",
"dev": true
"optional": true
},
"tinycolor2": {
"version": "1.4.1",
@@ -9717,7 +9720,6 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz",
"integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==",
"dev": true,
"requires": {
"rimraf": "^2.6.3"
},
@@ -9726,7 +9728,6 @@
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
"integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -9740,7 +9741,6 @@
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
@@ -9751,7 +9751,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-2.0.2.tgz",
"integrity": "sha512-zl71nFWjPKW2KXs+73gEk8RmqvtAeXPxhWDkTUoa3MSMkjq3I+9OeknjF178MQoMYsdqL730hfzvNfEkePxq9Q==",
"dev": true,
"optional": true,
"requires": {
"tmp": "0.1.0"
}
@@ -9986,12 +9986,19 @@
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"unused-filename": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-1.0.0.tgz",
"integrity": "sha1-00CID3GuIRXrqhMlvvBcxmhEacY=",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-2.1.0.tgz",
"integrity": "sha512-BMiNwJbuWmqCpAM1FqxCTD7lXF97AvfQC8Kr/DIeA6VtvhJaMDupZ82+inbjl5yVP44PcxOuCSxye1QMS0wZyg==",
"requires": {
"modify-filename": "^1.1.0",
"path-exists": "^3.0.0"
"path-exists": "^4.0.0"
},
"dependencies": {
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
}
}
},
"unzip-response": {
@@ -10288,8 +10295,7 @@
"which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
"dev": true
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
},
"wide-align": {
"version": "1.1.3",
@@ -10345,7 +10351,7 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"dev": true
"optional": true
},
"wordwrap": {
"version": "1.0.0",
@@ -10356,7 +10362,6 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
@@ -10366,14 +10371,12 @@
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
@@ -10381,14 +10384,12 @@
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
@@ -10399,7 +10400,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
@@ -10430,12 +10430,9 @@
}
},
"ws": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.2.0.tgz",
"integrity": "sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg==",
"requires": {
"async-limiter": "^1.0.0"
}
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.2.1.tgz",
"integrity": "sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A=="
},
"x-xss-protection": {
"version": "1.3.0",
@@ -10509,8 +10506,7 @@
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"dev": true
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
},
"yallist": {
"version": "3.1.1",
+11 -9
View File
@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.38.1-beta",
"version": "0.39.2-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -27,23 +27,23 @@
"commonmark": "0.29.0",
"cookie-parser": "1.4.4",
"csurf": "1.10.0",
"dayjs": "1.8.17",
"dayjs": "1.8.18",
"debug": "4.1.1",
"ejs": "2.7.4",
"electron-debug": "3.0.1",
"electron-dl": "1.14.0",
"electron-dl": "2.0.0",
"electron-find": "1.0.6",
"electron-spellchecker": "2.2.1",
"electron-window-state": "5.0.3",
"express": "4.17.1",
"express-session": "1.17.0",
"file-type": "12.4.0",
"file-type": "12.4.2",
"fs-extra": "8.1.0",
"helmet": "3.21.2",
"html": "1.0.0",
"html2plaintext": "2.1.2",
"http-proxy-agent": "2.1.0",
"https-proxy-agent": "3.0.1",
"http-proxy-agent": "3.0.0",
"https-proxy-agent": "4.0.0",
"image-type": "4.1.0",
"imagemin": "7.0.1",
"imagemin-giflossy": "5.1.10",
@@ -63,7 +63,7 @@
"rimraf": "3.0.0",
"sanitize-filename": "1.6.3",
"sax": "1.2.4",
"semver": "6.3.0",
"semver": "7.1.1",
"serve-favicon": "2.5.0",
"session-file-store": "1.3.1",
"simple-node-logger": "18.12.23",
@@ -74,15 +74,17 @@
"turndown": "5.0.3",
"turndown-plugin-gfm": "1.0.2",
"unescape": "1.0.1",
"ws": "7.2.0"
"ws": "7.2.1"
},
"devDependencies": {
"electron": "6.0.12",
"electron-builder": "22.1.0",
"electron-installer-debian": "2.0.1",
"electron-packager": "14.1.1",
"electron-rebuild": "1.8.8",
"jsdoc": "3.6.3",
"lorem-ipsum": "2.0.3"
},
"optionalDependencies": {
"electron-installer-debian": "2.0.1"
}
}
+52 -2
View File
@@ -31,6 +31,9 @@ import dateNoteService from './services/date_notes.js';
import sidebarService from './services/sidebar.js';
import importService from './services/import.js';
import keyboardActionService from "./services/keyboard_actions.js";
import splitService from "./services/split.js";
import optionService from "./services/options.js";
import noteContentRenderer from "./services/note_content_renderer.js";
window.glob.isDesktop = utils.isDesktop;
window.glob.isMobile = utils.isMobile;
@@ -39,6 +42,20 @@ window.glob.isMobile = utils.isMobile;
window.glob.getActiveNode = treeService.getActiveNode;
window.glob.getHeaders = server.getHeaders;
window.glob.showAddLinkDialog = () => import('./dialogs/add_link.js').then(d => d.showDialog());
window.glob.showIncludeNoteDialog = cb => import('./dialogs/include_note.js').then(d => d.showDialog(cb));
window.glob.loadIncludedNote = async (noteId, el) => {
const note = await treeCache.getNote(noteId);
if (note) {
$(el).empty().append($("<h3>").append(await linkService.createNoteLink(note.noteId, {
showTooltip: false
})));
const {renderedContent} = await noteContentRenderer.getRenderedContent(note);
$(el).append(renderedContent);
}
};
// this is required by CKEditor when uploading images
window.glob.noteChanged = noteDetailService.noteChanged;
window.glob.refreshTree = treeService.reload;
@@ -142,7 +159,10 @@ async function printActiveNote() {
$tabContext.$tabContent.find('.note-detail-component:visible').printThis({
header: $("<h2>").text($tabContext.note && $tabContext.note.title).prop('outerHTML') ,
importCSS: false,
loadCSS: "libraries/codemirror/codemirror.css",
loadCSS: [
"libraries/codemirror/codemirror.css",
"libraries/ckeditor/ckeditor-content.css"
],
debug: true
});
}
@@ -176,4 +196,34 @@ noteAutocompleteService.init();
if (utils.isElectron()) {
import("./services/spell_check.js").then(spellCheckService => spellCheckService.initSpellCheck());
}
}
optionService.waitForOptions().then(options => {
if (utils.isElectron() && !options.is('nativeTitleBarVisible')) {
$("#title-bar-buttons").show();
$("#minimize-btn").on('click', () => {
$("#minimize-btn").trigger('blur');
const {remote} = require('electron');
remote.BrowserWindow.getFocusedWindow().minimize();
});
$("#maximize-btn").on('click', () => {
$("#maximize-btn").trigger('blur');
const {remote} = require('electron');
const focusedWindow = remote.BrowserWindow.getFocusedWindow();
if (focusedWindow.isMaximized()) {
focusedWindow.unmaximize();
} else {
focusedWindow.maximize();
}
});
$("#close-btn").on('click', () => {
$("#close-btn").trigger('blur');
const {remote} = require('electron');
remote.BrowserWindow.getFocusedWindow().close();
});
}
});
@@ -0,0 +1,40 @@
import treeUtils from '../services/tree_utils.js';
import noteAutocompleteService from '../services/note_autocomplete.js';
import utils from "../services/utils.js";
const $dialog = $("#include-note-dialog");
const $form = $("#include-note-form");
const $autoComplete = $("#include-note-autocomplete");
let callback = null;
export async function showDialog(cb) {
callback = cb;
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$autoComplete.val('');
$dialog.modal();
noteAutocompleteService.initNoteAutocomplete($autoComplete, { hideGoToSelectedNoteButton: true });
noteAutocompleteService.showRecentNotes($autoComplete);
}
$form.on('submit', () => {
const notePath = $autoComplete.getSelectedPath();
if (notePath) {
$dialog.modal('hide');
if (callback) {
callback(treeUtils.getNoteIdFromNotePath(notePath));
}
}
else {
console.error("No noteId to include.");
}
return false;
});
@@ -19,12 +19,13 @@ const TPL = `
<input type="number" class="form-control" id="zoom-factor-select" min="0.3" max="2.0" step="0.1"/>
</div>
<div class="col-4">
<label for="one-tab-display-select">If there's only one tab, then...</label>
<select class="form-control" id="one-tab-display-select">
<option value="show">show the tab bar</option>
<option value="hide">hide the tab bar</option>
<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>
</select>
</div>
</div>
@@ -69,37 +70,6 @@ const TPL = `
</div>
<p>Note that tree and detail font sizing is relative to the main font size setting.</p>
<h4>Left pane sizing</h4>
<div class="form-group row">
<div class="col-6">
<label for="left-pane-min-width">Left pane minimum width (in pixels)</label>
<div class="input-group">
<input type="number" class="form-control" id="left-pane-min-width" min="100" max="2000" step="1"/>
<div class="input-group-append">
<span class="input-group-text">px</span>
</div>
</div>
</div>
<div class="col-6">
<label for="left-pane-min-width">Left pane width percent of window size</label>
<div class="input-group">
<input type="number" class="form-control" id="left-pane-width-percent" min="0" max="99" step="1"/>
<div class="input-group-append">
<span class="input-group-text">%</span>
</div>
</div>
</div>
</div>
<p>Left pane width is calculated from the percent of window size, if this is smaller than minimum width, then minimum width is used. If you want to have fixed width left pane, set minimum width to the desired width and set percent to 0.</p>
</form>`;
export default class ApperanceOptions {
@@ -108,9 +78,7 @@ export default class ApperanceOptions {
this.$themeSelect = $("#theme-select");
this.$zoomFactorSelect = $("#zoom-factor-select");
this.$oneTabDisplaySelect = $("#one-tab-display-select");
this.$leftPaneMinWidth = $("#left-pane-min-width");
this.$leftPaneWidthPercent = $("#left-pane-width-percent");
this.$nativeTitleBarSelect = $("#native-title-bar-select");
this.$mainFontSize = $("#main-font-size");
this.$treeFontSize = $("#tree-font-size");
this.$detailFontSize = $("#detail-font-size");
@@ -141,23 +109,10 @@ export default class ApperanceOptions {
this.$zoomFactorSelect.on('change', () => { zoomService.setZoomFactorAndSave(this.$zoomFactorSelect.val()); });
this.$oneTabDisplaySelect.on('change', () => {
const hideTabRowForOneTab = this.$oneTabDisplaySelect.val() === 'hide' ? 'true' : 'false';
this.$nativeTitleBarSelect.on('change', () => {
const nativeTitleBarVisible = this.$nativeTitleBarSelect.val() === 'show' ? 'true' : 'false';
server.put('options/hideTabRowForOneTab/' + hideTabRowForOneTab)
.then(optionsService.reloadOptions);
});
this.$leftPaneMinWidth.on('change', async () => {
await server.put('options/leftPaneMinWidth/' + this.$leftPaneMinWidth.val());
this.resizeLeftPanel();
});
this.$leftPaneWidthPercent.on('change', async () => {
await server.put('options/leftPaneWidthPercent/' + this.$leftPaneWidthPercent.val());
this.resizeLeftPanel();
server.put('options/nativeTitleBarVisible/' + nativeTitleBarVisible);
});
this.$mainFontSize.on('change', async () => {
@@ -204,24 +159,13 @@ export default class ApperanceOptions {
this.$zoomFactorSelect.prop('disabled', true);
}
this.$oneTabDisplaySelect.val(options.hideTabRowForOneTab === 'true' ? 'hide' : 'show');
this.$leftPaneMinWidth.val(options.leftPaneMinWidth);
this.$leftPaneWidthPercent.val(options.leftPaneWidthPercent);
this.$nativeTitleBarSelect.val(options.nativeTitleBarVisible === 'true' ? 'show' : 'hide');
this.$mainFontSize.val(options.mainFontSize);
this.$treeFontSize.val(options.treeFontSize);
this.$detailFontSize.val(options.detailFontSize);
}
resizeLeftPanel() {
const leftPanePercent = parseInt(this.$leftPaneWidthPercent.val());
const rightPanePercent = 100 - leftPanePercent;
const leftPaneMinWidth = this.$leftPaneMinWidth.val();
this.$container.css("grid-template-columns", `minmax(${leftPaneMinWidth}px, ${leftPanePercent}fr) ${rightPanePercent}fr`);
}
applyFontSizes() {
this.$body.get(0).style.setProperty("--main-font-size", this.$mainFontSize.val() + "%");
this.$body.get(0).style.setProperty("--tree-font-size", this.$treeFontSize.val() + "%");
@@ -3,45 +3,6 @@ import server from "../../services/server.js";
import optionsService from "../../services/options.js";
const TPL = `
<h4>Show sidebar in new tab</h4>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="show-sidebar-in-new-tab">
<label class="form-check-label" for="show-sidebar-in-new-tab">Show sidebar in new tab</label>
</div>
<br>
<h4>Sidebar sizing</h4>
<div class="form-group row">
<div class="col-6">
<label for="sidebar-min-width">Sidebar minimum width (in pixels)</label>
<div class="input-group">
<input type="number" class="form-control" id="sidebar-min-width" min="100" max="2000" step="1"/>
<div class="input-group-append">
<span class="input-group-text">px</span>
</div>
</div>
</div>
<div class="col-6">
<label for="left-pane-min-width">Sidebar width percent of the detail pane</label>
<div class="input-group">
<input type="number" class="form-control" id="sidebar-width-percent" min="0" max="70" step="1"/>
<div class="input-group-append">
<span class="input-group-text">%</span>
</div>
</div>
</div>
</div>
<p>Sidebar width is calculated from the percent of the detail pane, if this is smaller than minimum width, then minimum width is used. If you want to have fixed width sidebar, set minimum width to the desired width and set percent to 0.</p>
<h4>Widgets</h4>
<div id="widgets-configuration" class="row">
@@ -58,48 +19,15 @@ export default class SidebarOptions {
constructor() {
$("#options-sidebar").html(TPL);
this.$sidebarMinWidth = $("#sidebar-min-width");
this.$sidebarWidthPercent = $("#sidebar-width-percent");
this.$showSidebarInNewTab = $("#show-sidebar-in-new-tab");
this.$widgetsConfiguration = $("#widgets-configuration");
this.$widgetsEnabled = $("#widgets-enabled");
this.$widgetsDisabled = $("#widgets-disabled");
this.$sidebarMinWidth.on('change', async () => {
await server.put('options/sidebarMinWidth/' + this.$sidebarMinWidth.val());
this.resizeSidebar();
});
this.$sidebarWidthPercent.on('change', async () => {
await server.put('options/sidebarWidthPercent/' + this.$sidebarWidthPercent.val());
this.resizeSidebar();
});
this.$showSidebarInNewTab.on('change', async () => {
const flag = this.$showSidebarInNewTab.is(":checked") ? 'true' : 'false';
await server.put('options/showSidebarInNewTab/' + flag);
optionsService.reloadOptions();
});
}
async optionsLoaded(options) {
this.$widgetsEnabled.empty();
this.$widgetsDisabled.empty();
this.$sidebarMinWidth.val(options.sidebarMinWidth);
this.$sidebarWidthPercent.val(options.sidebarWidthPercent);
if (options.showSidebarInNewTab === 'true') {
this.$showSidebarInNewTab.attr("checked", "checked");
}
else {
this.$showSidebarInNewTab.removeAttr("checked");
}
const widgets = [
{name: 'attributes', title: 'Attributes'},
{name: 'linkMap', title: 'Link map'},
@@ -188,19 +116,4 @@ export default class SidebarOptions {
return null;
}
}
resizeSidebar() {
const sidebarWidthPercent = parseInt(this.$sidebarWidthPercent.val());
const sidebarMinWidth = this.$sidebarMinWidth.val();
// need to find them dynamically since they change
const $sidebar = $(".note-detail-sidebar");
const $content = $(".note-detail-content");
$sidebar.css("width", sidebarWidthPercent + '%');
$sidebar.css("min-width", sidebarMinWidth + 'px');
$content.css("width", (100 - sidebarWidthPercent) + '%');
}
}
@@ -44,7 +44,10 @@ export async function showDialog() {
const note = await treeCache.getNote(change.noteId);
const notePath = await treeService.getSomeNotePath(note);
noteLink = await linkService.createNoteLinkWithPath(notePath, change.title);
noteLink = await linkService.createNoteLink(notePath, {
title: change.title,
showNotePath: true
});
}
changesListEl.append($('<li>')
@@ -6,7 +6,6 @@ class Branch {
this.branchId = row.branchId;
/** @param {string} */
this.noteId = row.noteId;
this.note = null;
/** @param {string} */
this.parentNoteId = row.parentNoteId;
/** @param {int} */
+23 -8
View File
@@ -9,6 +9,7 @@ import noteDetailService from "./note_detail.js";
import keyboardActionService from "./keyboard_actions.js";
import hoistedNoteService from "./hoisted_note.js";
import treeCache from "./tree_cache.js";
import server from "./server.js";
const NOTE_REVISIONS = "../dialogs/note_revisions.js";
const OPTIONS = "../dialogs/options.js";
@@ -111,10 +112,21 @@ function registerEntrypoints() {
keyboardActionService.setGlobalActionHandler("ForwardInNoteHistory", window.history.forward);
}
let zenModeActive = false;
// hide (toggle) everything except for the note content for zen mode
const toggleZenMode = () => {
$(".hide-in-zen-mode").toggle();
$("#container").toggleClass("zen-mode");
if (!zenModeActive) {
$(".hide-in-zen-mode,.gutter").addClass("hidden-by-zen-mode");
$("#container").addClass("zen-mode");
zenModeActive = true;
}
else {
// not hiding / showing explicitly since element might be hidden also for other reasons
$(".hide-in-zen-mode,.gutter").removeClass("hidden-by-zen-mode");
$("#container").removeClass("zen-mode");
zenModeActive = false;
}
};
$("#toggle-zen-mode-button").on('click', toggleZenMode);
@@ -232,16 +244,19 @@ function registerEntrypoints() {
keyboardActionService.setGlobalActionHandler("CreateNoteIntoDayNote", async () => {
const todayNote = await dateNoteService.getTodayNote();
const notePath = await treeService.getSomeNotePath(todayNote);
const node = await treeService.expandToNote(notePath);
const {note} = await treeService.createNote(node, todayNote.noteId, 'into', {
type: "text",
isProtected: node.data.isProtected
const {note} = await server.post(`notes/${todayNote.noteId}/children?target=into`, {
title: 'new note',
content: '',
type: 'text',
isProtected: todayNote.isProtected
});
await treeService.expandToNote(note.noteId);
await noteDetailService.openInTab(note.noteId, true);
noteDetailService.focusAndSelectTitle();
});
keyboardActionService.setGlobalActionHandler("EditBranchPrefix", async () => {
@@ -125,14 +125,14 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
}
/**
* Executes given anonymous function on the server.
* Executes given anonymous function on the backend.
* Internally this serializes the anonymous function into string and sends it to backend via AJAX.
*
* @param {string} script - script to be executed on the backend
* @param {Array.<?>} params - list of parameters to the anonymous function to be send to backend
* @return {Promise<*>} return value of the executed function on the backend
*/
this.runOnServer = async (script, params = []) => {
this.runOnBackend = async (script, params = []) => {
if (typeof script === "function") {
script = script.toString();
}
@@ -159,6 +159,12 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
}
};
/**
* @deprecated new name of this API call is runOnBackend so use that
* @method
*/
this.runOnServer = this.runOnBackend;
/**
* This is a powerful search method - you can search by attributes and their values, e.g.:
* "@dateModified =* MONTH AND @log". See full documentation for all options at: https://github.com/zadam/trilium/wiki/Search
+38 -42
View File
@@ -6,15 +6,14 @@ import noteDetailService from "./note_detail.js";
function getNotePathFromUrl(url) {
const notePathMatch = /#(root[A-Za-z0-9/]*)$/.exec(url);
if (notePathMatch === null) {
return null;
}
else {
return notePathMatch[1];
}
return notePathMatch === null ? null : notePathMatch[1];
}
async function createNoteLink(notePath, noteTitle = null, tooltip = true) {
async function createNoteLink(notePath, options = {}) {
let noteTitle = options.title;
const showTooltip = options.showTooltip === undefined ? true : options.showTooltip;
const showNotePath = options.showNotePath === undefined ? false : options.showNotePath;
if (!noteTitle) {
const {noteId, parentNoteId} = treeUtils.getNoteIdAndParentIdFromNotePath(notePath);
@@ -27,30 +26,26 @@ async function createNoteLink(notePath, noteTitle = null, tooltip = true) {
}).attr('data-action', 'note')
.attr('data-note-path', notePath);
if (!tooltip) {
if (!showTooltip) {
$noteLink.addClass("no-tooltip-preview");
}
return $noteLink;
}
const $container = $("<span>").append($noteLink);
async function createNoteLinkWithPath(notePath, noteTitle = null) {
const $link = await createNoteLink(notePath, noteTitle);
if (showNotePath) {
notePath = await treeService.resolveNotePath(notePath);
const $res = $("<span>").append($link);
if (notePath.includes("/")) {
const noteIds = notePath.split("/");
noteIds.pop(); // remove last element
const parentNotePath = noteIds.join("/").trim();
if (parentNotePath) {
$res.append($("<small>").text(" (" + await treeUtils.getNotePathTitle(parentNotePath) + ")"));
$container.append($("<small>").text(" (" + await treeUtils.getNotePathTitle(parentNotePath) + ")"));
}
}
return $res;
return $container;
}
function getNotePathFromLink($link) {
@@ -66,7 +61,10 @@ function getNotePathFromLink($link) {
}
function goToLink(e) {
const $link = $(e.target);
e.preventDefault();
e.stopPropagation();
const $link = $(e.target).closest("a");
const notePath = getNotePathFromLink($link);
@@ -89,9 +87,6 @@ function goToLink(e) {
}
}
e.preventDefault();
e.stopPropagation();
return true;
}
@@ -118,7 +113,7 @@ function addTextToEditor(text) {
}
function newTabContextMenu(e) {
const $link = $(e.target);
const $link = $(e.target).closest("a");
const notePath = getNotePathFromLink($link);
@@ -142,22 +137,27 @@ function newTabContextMenu(e) {
});
}
$(document).on('contextmenu', '.note-detail-text a', newTabContextMenu);
$(document).on('contextmenu', "a[data-action='note']", newTabContextMenu);
$(document).on('contextmenu', ".note-detail-render a", newTabContextMenu);
// when click on link popup, in case of internal link, just go the the referenced note instead of default behavior
// of opening the link in new window/tab
$(document).on('mousedown', "a[data-action='note']", goToLink);
$(document).on('mousedown', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
$(document).on('dblclick', '.note-detail-text a', goToLink);
$(document).on('mousedown', '.note-detail-text a', function (e) {
const notePath = getNotePathFromLink($(e.target));
if (notePath && ((e.which === 1 && e.ctrlKey) || e.which === 2)) {
const $link = $(e.target).closest("a");
const notePath = getNotePathFromLink($link);
if ((e.which === 1 && e.ctrlKey) || e.which === 2) {
// if it's a ctrl-click, then we open on new tab, otherwise normal flow (CKEditor opens link-editing dialog)
e.preventDefault();
noteDetailService.loadNoteDetail(notePath, { newTab: true });
if (notePath) {
noteDetailService.loadNoteDetail(notePath, {newTab: true});
}
else {
const address = $link.attr('href');
window.open(address, '_blank');
}
return true;
}
@@ -166,24 +166,20 @@ $(document).on('mousedown', '.note-detail-text a', function (e) {
$(document).on('mousedown', '.note-detail-book a', goToLink);
$(document).on('mousedown', '.note-detail-render a', goToLink);
$(document).on('mousedown', '.note-detail-text.ck-read-only a', goToLink);
$(document).on('mousedown', 'span.ck-button__label', e => {
// this is a link preview dialog from CKEditor link editing
// for some reason clicked element is span
const url = $(e.target).text();
const notePath = getNotePathFromUrl(url);
if (notePath) {
treeService.activateNote(notePath);
e.preventDefault();
}
$(document).on('mousedown', 'a.ck-link-actions__preview', goToLink);
$(document).on('click', 'a.ck-link-actions__preview', e => {
e.preventDefault();
e.stopPropagation();
});
$(document).on('contextmenu', 'a.ck-link-actions__preview', newTabContextMenu);
$(document).on('contextmenu', '.note-detail-text a', newTabContextMenu);
$(document).on('contextmenu', "a[data-action='note']", newTabContextMenu);
$(document).on('contextmenu', ".note-detail-render a", newTabContextMenu);
export default {
getNotePathFromUrl,
createNoteLink,
createNoteLinkWithPath,
addLinkToEditor,
addTextToEditor,
goToLink
+1 -1
View File
@@ -88,7 +88,7 @@ export default class LinkMap {
.addClass("note-box")
.prop("id", noteBoxId);
linkService.createNoteLink(noteId, note.title).then($link => {
linkService.createNoteLink(noteId, {title: note.title}).then($link => {
$link.on('click', e => {
try {
$link.tooltip('dispose');
@@ -0,0 +1,107 @@
import server from "./server.js";
import utils from "./utils.js";
import renderService from "./render.js";
import protectedSessionService from "./protected_session.js";
import protectedSessionHolder from "./protected_session_holder.js";
async function getRenderedContent(note) {
const type = getRenderingType(note);
let rendered;
if (type === 'text') {
const fullNote = await server.get('notes/' + note.noteId);
const $content = $("<div>").html(fullNote.content);
if (utils.isHtmlEmpty(fullNote.content)) {
rendered = "";
}
else {
rendered = $content;
}
}
else if (type === 'code') {
const fullNote = await server.get('notes/' + note.noteId);
if (fullNote.content.trim() === "") {
rendered = "";
}
rendered = $("<pre>").text(fullNote.content);
}
else if (type === 'image') {
rendered = $("<img>").attr("src", `api/images/${note.noteId}/${note.title}`);
}
else if (type === 'file') {
function getFileUrl() {
return utils.getUrlForDownload("api/notes/" + note.noteId + "/download");
}
const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>');
const $openButton = $('<button class="file-open btn btn-primary" type="button">Open</button>');
$downloadButton.on('click', () => utils.download(getFileUrl()));
$openButton.on('click', () => {
if (utils.isElectron()) {
const open = require("open");
open(getFileUrl(), {url: true});
}
else {
window.location.href = getFileUrl();
}
});
// open doesn't work for protected notes since it works through browser which isn't in protected session
$openButton.toggle(!note.isProtected);
rendered = $('<div>')
.append($downloadButton)
.append(' &nbsp; ')
.append($openButton);
}
else if (type === 'render') {
const $el = $('<div>');
await renderService.render(note, $el, this.ctx);
rendered = $el;
}
else if (type === 'protected-session') {
const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`)
.on('click', protectedSessionService.enterProtectedSession);
rendered = $("<div>")
.append("<div>This note is protected and to access it you need to enter password.</div>")
.append("<br/>")
.append($button);
}
else {
rendered = "<em>Content of this note cannot be displayed in the book format</em>";
}
return {
renderedContent: rendered,
type
};
}
function getRenderingType(note) {
let type = note.type;
if (note.isProtected) {
if (protectedSessionHolder.isProtectedSessionAvailable()) {
protectedSessionHolder.touchProtectedSession();
}
else {
type = 'protected-session';
}
}
return type;
}
export default {
getRenderedContent
};
@@ -1,10 +1,6 @@
import server from "./server.js";
import linkService from "./link.js";
import utils from "./utils.js";
import treeCache from "./tree_cache.js";
import renderService from "./render.js";
import protectedSessionHolder from "./protected_session_holder.js";
import protectedSessionService from "./protected_session.js";
import noteContentRenderer from "./note_content_renderer.js";
const MIN_ZOOM_LEVEL = 1;
const MAX_ZOOM_LEVEL = 6;
@@ -129,18 +125,18 @@ class NoteDetailBook {
async renderIntoElement(note, $container) {
for (const childNote of await note.getChildNotes()) {
const type = this.getRenderingType(childNote);
const childNotePath = this.ctx.notePath + '/' + childNote.noteId;
const {type, renderedContent} = await noteContentRenderer.getRenderedContent(childNote);
const $card = $('<div class="note-book-card">')
.attr('data-note-id', childNote.noteId)
.css("flex-basis", ZOOMS[this.zoomLevel].width)
.addClass("type-" + type)
.append($('<h5 class="note-book-title">').append(await linkService.createNoteLink(childNotePath, null, false)))
.append($('<h5 class="note-book-title">').append(await linkService.createNoteLink(childNotePath, {showTooltip: false})))
.append($('<div class="note-book-content">')
.css("max-height", ZOOMS[this.zoomLevel].height)
.append(await this.getNoteContent(type, childNote)));
.append(renderedContent));
const childCount = childNote.getChildNoteIds().length;
@@ -158,80 +154,6 @@ class NoteDetailBook {
}
}
async getNoteContent(type, note) {
if (type === 'text') {
const fullNote = await server.get('notes/' + note.noteId);
const $content = $("<div>").html(fullNote.content);
if (utils.isHtmlEmpty(fullNote.content)) {
return "";
}
else {
return $content;
}
}
else if (type === 'code') {
const fullNote = await server.get('notes/' + note.noteId);
if (fullNote.content.trim() === "") {
return "";
}
return $("<pre>").text(fullNote.content);
}
else if (type === 'image') {
return $("<img>").attr("src", `api/images/${note.noteId}/${note.title}`);
}
else if (type === 'file') {
function getFileUrl() {
return utils.getUrlForDownload("api/notes/" + note.noteId + "/download");
}
const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>');
const $openButton = $('<button class="file-open btn btn-primary" type="button">Open</button>');
$downloadButton.on('click', () => utils.download(getFileUrl()));
$openButton.on('click', () => {
if (utils.isElectron()) {
const open = require("open");
open(getFileUrl(), {url: true});
}
else {
window.location.href = getFileUrl();
}
});
// open doesn't work for protected notes since it works through browser which isn't in protected session
$openButton.toggle(!note.isProtected);
return $('<div>')
.append($downloadButton)
.append(' &nbsp; ')
.append($openButton);
}
else if (type === 'render') {
const $el = $('<div>');
await renderService.render(note, $el, this.ctx);
return $el;
}
else if (type === 'protected-session') {
const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`)
.on('click', protectedSessionService.enterProtectedSession);
return $("<div>")
.append("<div>This note is protected and to access it you need to enter password.</div>")
.append("<br/>")
.append($button);
}
else {
return "<em>Content of this note cannot be displayed in the book format</em>";
}
}
/** @return {boolean} true if this is "auto book" activated (empty text note) and not explicit book note */
isAutoBook() {
return this.ctx.note.type !== 'book';
@@ -256,21 +178,6 @@ class NoteDetailBook {
}
}
getRenderingType(childNote) {
let type = childNote.type;
if (childNote.isProtected) {
if (protectedSessionHolder.isProtectedSessionAvailable()) {
protectedSessionHolder.touchProtectedSession();
}
else {
type = 'protected-session';
}
}
return type;
}
getContent() {
// for auto-book cases when renaming title there should be content
return "";
@@ -39,8 +39,11 @@ class NoteDetailFile {
});
this.$uploadNewRevisionInput.on('change', async () => {
const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below
this.$uploadNewRevisionInput.val('');
const formData = new FormData();
formData.append('upload', this.$uploadNewRevisionInput[0].files[0]);
formData.append('upload', fileToUpload);
const result = await $.ajax({
url: baseApiUrl + 'notes/' + this.ctx.note.noteId + '/file',
@@ -48,8 +48,11 @@ class NoteDetailImage {
});
this.$uploadNewRevisionInput.on('change', async () => {
const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below
this.$uploadNewRevisionInput.val('');
const formData = new FormData();
formData.append('upload', this.$uploadNewRevisionInput[0].files[0]);
formData.append('upload', fileToUpload);
const result = await $.ajax({
url: baseApiUrl + 'images/' + this.ctx.note.noteId,
@@ -129,9 +129,10 @@ class NoteDetailRelationMap {
return;
}
const {note} = await server.post(`notes/${this.ctx.note.noteId}/children`, {
const {note} = await server.post(`notes/${this.ctx.note.noteId}/children?target=into`, {
title,
target: 'into'
content: '',
type: 'text'
});
toastService.showMessage("Click on canvas to place new note");
@@ -493,7 +494,7 @@ class NoteDetailRelationMap {
}
async createNoteBox(noteId, title, x, y) {
const $link = await linkService.createNoteLink(noteId, title);
const $link = await linkService.createNoteLink(noteId, {title});
$link.mousedown(e => {
console.log(e);
@@ -2,6 +2,7 @@ import libraryLoader from "./library_loader.js";
import treeService from './tree.js';
import noteAutocompleteService from './note_autocomplete.js';
import mimeTypesService from './mime_types.js';
import treeCache from "./tree_cache.js";
const mentionSetup = {
feeds: [
@@ -61,7 +62,7 @@ class NoteDetailText {
else {
window.open(src, '_blank');
}
})
});
}
async render() {
@@ -94,6 +95,11 @@ class NoteDetailText {
}
});
if (glob.isDev) {
await import('../../libraries/ckeditor/inspector.js');
CKEditorInspector.attach(this.textEditor);
}
this.onNoteChange(() => this.ctx.noteChanged());
}
}
+59 -18
View File
@@ -1,6 +1,47 @@
import bundleService from "./bundle.js";
import ws from "./ws.js";
import optionsService from "./options.js";
import splitService from "./split.js";
import optionService from "./options.js";
import server from "./server.js";
import noteDetailService from "./note_detail.js";
const $sidebar = $("#right-pane");
const $sidebarContainer = $('#sidebar-container');
const $showSidebarButton = $("#show-sidebar-button");
const $hideSidebarButton = $("#hide-sidebar-button");
optionService.waitForOptions().then(options => toggleSidebar(options.is('rightPaneVisible')));
function toggleSidebar(show) {
$sidebar.toggle(show);
$showSidebarButton.toggle(!show);
$hideSidebarButton.toggle(show);
if (show) {
splitService.setupSplitWithSidebar();
}
else {
splitService.setupSplitWithoutSidebar();
}
}
$hideSidebarButton.on('click', () => {
toggleSidebar(false);
server.put('options/rightPaneVisible/false');
});
$showSidebarButton.on('click', async () => {
toggleSidebar(true);
await server.put('options/rightPaneVisible/true');
const {sidebar} = noteDetailService.getActiveTabContext();
await sidebar.noteLoaded();
sidebar.show();
});
class Sidebar {
/**
@@ -14,30 +55,18 @@ class Sidebar {
widgets: []
}, state);
this.widgets = [];
this.$sidebar = ctx.$tabContent.find(".note-detail-sidebar");
this.$widgetContainer = this.$sidebar.find(".note-detail-widget-container");
this.$showSideBarButton = this.ctx.$tabContent.find(".show-sidebar-button");
this.$hideSidebarButton = this.$sidebar.find(".hide-sidebar-button");
this.$hideSidebarButton.on('click', () => {
this.$sidebar.hide();
this.$showSideBarButton.show();
this.ctx.stateChanged();
});
this.$widgetContainer = $sidebar.find(`.sidebar-widget-container[data-tab-id=${this.ctx.tabId}]`);
this.$showSideBarButton.on('click', () => {
this.$sidebar.show();
this.$showSideBarButton.hide();
this.ctx.stateChanged();
this.noteLoaded();
});
if (this.$widgetContainer.length === 0) {
this.$widgetContainer = $(`<div class="sidebar-widget-container">`).attr('data-tab-id', this.ctx.tabId);
this.$showSideBarButton.toggle(!state.visible);
this.$sidebar.toggle(state.visible);
$sidebarContainer.append(this.$widgetContainer);
}
}
isVisible() {
return this.$sidebar.css("display") !== "none";
return $sidebar.css("display") !== "none";
}
getSidebarState() {
@@ -91,6 +120,12 @@ class Sidebar {
this.renderWidgets(widgets);
}
show() {
$sidebarContainer.find('.sidebar-widget-container').each((i, el) => {
$(el).toggle($(el).attr('data-tab-id') === this.ctx.tabId);
});
}
// it's important that this method is sync so that the whole render-update is atomic
// otherwise we could get race issues (doubled widgets etc.)
renderWidgets(widgets) {
@@ -119,6 +154,12 @@ class Sidebar {
this.$widgetContainer.empty().append(...widgetsToAppend);
}
remove() {
if (this.$widgetContainer) {
this.$widgetContainer.remove();
}
}
eventReceived(name, data) {
for (const widget of this.widgets) {
if (widget.eventReceived) {
+51
View File
@@ -0,0 +1,51 @@
import server from "./server.js";
import optionService from "./options.js";
let instance;
async function getPaneWidths() {
const options = await optionService.waitForOptions();
return {
leftPaneWidth: options.getInt('leftPaneWidth'),
rightPaneWidth: options.getInt('rightPaneWidth')
};
}
async function setupSplitWithSidebar() {
if (instance) {
instance.destroy();
}
const {leftPaneWidth, rightPaneWidth} = await getPaneWidths();
instance = Split(['#left-pane', '#center-pane', '#right-pane'], {
sizes: [leftPaneWidth, 100 - leftPaneWidth - rightPaneWidth, rightPaneWidth],
gutterSize: 5,
onDragEnd: sizes => {
server.put('options/leftPaneWidth/' + Math.round(sizes[0]));
server.put('options/rightPaneWidth/' + Math.round(sizes[2]));
}
});
}
async function setupSplitWithoutSidebar() {
if (instance) {
instance.destroy();
}
const {leftPaneWidth} = await getPaneWidths();
instance = Split(['#left-pane', '#center-pane'], {
sizes: [leftPaneWidth, 100 - leftPaneWidth],
gutterSize: 5,
onDragEnd: sizes => {
server.put('options/leftPaneWidth/' + Math.round(sizes[0]));
}
});
}
export default {
setupSplitWithSidebar,
setupSplitWithoutSidebar
};
+27 -5
View File
@@ -148,6 +148,10 @@ class TabContext {
this.setCurrentNotePathToHash();
if (this.sidebar) {
this.sidebar.noteLoaded(); // load async
}
this.noteChangeDisabled = true;
try {
@@ -181,10 +185,6 @@ class TabContext {
this.noteType.update();
}
if (this.sidebar) {
this.sidebar.noteLoaded(); // load async
}
bundleService.executeRelationBundles(this.note, 'runOnNoteView', this);
}
@@ -204,6 +204,10 @@ class TabContext {
this.$tabContent.show();
if (this.sidebar) {
this.sidebar.show();
}
this.setCurrentNotePathToHash();
this.setTitleBar();
}
@@ -285,6 +289,10 @@ class TabContext {
}
getComponent() {
if (!this.components[this.type]) {
throw new Error("Could not find component for type: " + this.type);
}
return this.components[this.type];
}
@@ -371,7 +379,7 @@ class TabContext {
async addPath(notePath, isCurrent) {
const title = await treeUtils.getNotePathTitle(notePath);
const noteLink = await linkService.createNoteLink(notePath, title);
const noteLink = await linkService.createNoteLink(notePath, {title});
noteLink
.addClass("no-tooltip-preview")
@@ -411,6 +419,16 @@ class TabContext {
await this.addPath(notePath, isCurrent);
}
const cloneLink = $("<div>")
.addClass("dropdown-item")
.append(
$('<button class="btn btn-sm">')
.text('Clone note to new location...')
.on('click', () => import("../dialogs/clone_to.js").then(d => d.showDialog([this.note.noteId])))
);
this.$notePathList.append(cloneLink);
}
}
@@ -422,6 +440,10 @@ class TabContext {
await this.saveNoteIfChanged();
this.$tabContent.remove();
}
if (this.sidebar) {
this.sidebar.remove();
}
}
cleanup() {
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -127,16 +127,16 @@ async function getNodeFromPath(notePath, expand = false, expandOpts = {}) {
// we expand only after hoisted note since before then nodes are not actually present in the tree
if (parentNode) {
checkFolderStatus(parentNode);
if (!parentNode.isLoaded()) {
await parentNode.load();
}
if (expand) {
parentNode.setExpanded(true, expandOpts);
await parentNode.setExpanded(true, expandOpts);
}
await checkFolderStatus(parentNode);
let foundChildNode = findChildNode(parentNode, childNoteId);
if (!foundChildNode) { // note might be recently created so we'll force reload and try again
@@ -67,7 +67,7 @@ async function prepareNode(branch) {
const note = await branch.getNote();
if (!note) {
console.log(`Branch has no note: ${branch}`);
throw new Error(`Branch has no note ` + branch.noteId);
}
const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
@@ -133,7 +133,7 @@ class TreeCache {
/** @return {Promise<NoteShort[]>} */
async getNotes(noteIds, silentNotFoundError = false) {
const missingNoteIds = noteIds.filter(noteId => this.notes[noteId] === undefined);
const missingNoteIds = noteIds.filter(noteId => !this.notes[noteId]);
if (missingNoteIds.length > 0) {
await this.reloadNotes(missingNoteIds);
+35 -17
View File
@@ -59,18 +59,25 @@ async function handleMessage(event) {
syncDataQueue.push(...syncRows);
// we set lastAcceptedSyncId even before sync processing and send ping so that backend can start sending more updates
lastAcceptedSyncId = Math.max(lastAcceptedSyncId, syncRows[syncRows.length - 1].id);
sendPing();
// first wait for all the preceding consumers to finish
while (consumeQueuePromise) {
await consumeQueuePromise;
}
// it's my turn so start it up
consumeQueuePromise = consumeSyncData();
try {
// it's my turn so start it up
consumeQueuePromise = consumeSyncData();
await consumeQueuePromise;
// finish and set to null to signal somebody else can pick it up
consumeQueuePromise = null;
await consumeQueuePromise;
}
finally {
// finish and set to null to signal somebody else can pick it up
consumeQueuePromise = null;
}
}
}
else if (message.type === 'sync-hash-check-failed') {
@@ -113,6 +120,15 @@ function checkSyncIdListeners() {
.forEach(l => console.log(`Waiting for syncId ${l.desiredSyncId} while current is ${lastProcessedSyncId} for ${Math.floor((Date.now() - l.start) / 1000)}s`));
}
async function runSafely(syncHandler, syncData) {
try {
return await syncHandler(syncData);
}
catch (e) {
console.log(`Sync handler failed with ${e.message}: ${e.stack}`);
}
}
async function consumeSyncData() {
if (syncDataQueue.length > 0) {
const allSyncData = syncDataQueue;
@@ -120,15 +136,19 @@ async function consumeSyncData() {
const outsideSyncData = allSyncData.filter(sync => sync.sourceId !== glob.sourceId);
// we set lastAcceptedSyncId even before sync processing and send ping so that backend can start sending more updates
lastAcceptedSyncId = Math.max(lastAcceptedSyncId, allSyncData[allSyncData.length - 1].id);
sendPing();
try {
// the update process should be synchronous as a whole but individual handlers can run in parallel
await Promise.all([
...allSyncMessageHandlers.map(syncHandler => runSafely(syncHandler, allSyncData)),
...outsideSyncMessageHandlers.map(syncHandler => runSafely(syncHandler, outsideSyncData))
]);
}
catch (e) {
logError(`Encountered error ${e.message}, reloading frontend.`);
// the update process should be synchronous as a whole but individual handlers can run in parallel
await Promise.all([
...allSyncMessageHandlers.map(syncHandler => syncHandler(allSyncData)),
...outsideSyncMessageHandlers.map(syncHandler => syncHandler(outsideSyncData))
]);
// if there's an error in updating the frontend then the easy option to recover is to reload the frontend completely
utils.reloadApp();
}
lastProcessedSyncId = Math.max(lastProcessedSyncId, allSyncData[allSyncData.length - 1].id);
}
@@ -152,7 +172,7 @@ function connectWebSocket() {
async function sendPing() {
if (Date.now() - lastPingTs > 30000) {
console.log(utils.now(), "Lost connection to server");
console.log(utils.now(), "Lost websocket connection to the backend");
}
if (ws.readyState === ws.OPEN) {
@@ -171,8 +191,6 @@ async function sendPing() {
setTimeout(() => {
ws = connectWebSocket();
lastAcceptedSyncId = glob.maxSyncIdAtLoad;
lastProcessedSyncId = glob.maxSyncIdAtLoad;
lastPingTs = Date.now();
setInterval(sendPing, 1000);
+8 -5
View File
@@ -81,7 +81,7 @@ function SetupModel() {
password: password1,
theme: theme
}).then(() => {
window.location.replace("./");
window.location.replace("./setup");
});
}
else if (this.setupType() === 'sync-from-server') {
@@ -131,12 +131,15 @@ async function checkOutstandingSyncs() {
const { stats, initialized } = await $.get('api/sync/stats');
if (initialized) {
window.location.replace("./");
const remote = require('electron').remote;
remote.app.relaunch();
remote.app.exit(0);
}
else {
const totalOutstandingSyncs = stats.outstandingPushes + stats.outstandingPulls;
const totalOutstandingSyncs = stats.outstandingPushes + stats.outstandingPulls;
$("#outstanding-syncs").html(totalOutstandingSyncs);
$("#outstanding-syncs").html(totalOutstandingSyncs);
}
}
function showAlert(message) {
@@ -45,7 +45,7 @@ class EditedNotesWidget extends StandardWidget {
$item.append($("<i>").text(editedNote.title + " (deleted)"));
}
else {
$item.append(editedNote.notePath ? await linkService.createNoteLinkWithPath(editedNote.notePath.join("/")) : editedNote.title);
$item.append(editedNote.notePath ? await linkService.createNoteLink(editedNote.notePath.join("/"), {showNotePath: true}) : editedNote.title);
}
$list.append($item);
@@ -39,7 +39,7 @@ class SimilarNotesWidget extends StandardWidget {
}
const $item = $("<li>")
.append(await linkService.createNoteLinkWithPath(similarNote.notePath.join("/")));
.append(await linkService.createNoteLink(similarNote.notePath.join("/"), {showNotePath: true}));
$list.append($item);
}
+71 -53
View File
@@ -6,31 +6,43 @@ body {
margin: 0 auto; /* center */
height: 100vh;
display: grid;
grid-template-areas: "header header"
"left-pane tabs"
"left-pane tab-container";
grid-template-rows: auto
auto
1fr;
justify-content: center;
grid-gap: 0;
display: flex;
flex-direction: column;
}
#container.zen-mode {
grid-template-areas:
"tab-container" !important;
grid-template-rows: auto
auto
!important;
grid-template-columns: 1fr !important;
#topbar {
display: flex;
}
.hidden-by-zen-mode {
display: none !important;
}
.gutter {
background: linear-gradient(to bottom, transparent, var(--accented-background-color), transparent);
}
.gutter:hover {
background: linear-gradient(to bottom, transparent, var(--main-border-color), transparent);
}
.gutter.gutter-horizontal {
cursor: col-resize;
}
.gutter.gutter-vertical {
cursor: row-resize;
}
#center-pane {
display: flex;
flex-direction: column;
}
#note-tab-container {
grid-area: tab-container;
min-height: 0;
padding-left: 10px;
width: 100%;
}
#search-box {
@@ -49,20 +61,19 @@ body {
}
#left-pane {
grid-area: left-pane;
height: 100%;
display: flex;
flex-direction: column;
}
#header {
grid-area: header;
#toolbar {
background-color: var(--header-background-color);
display: flex;
align-items: center;
padding-top: 4px;
}
#header button {
#toolbar button {
padding: 1px 5px 1px 5px;
font-size: smaller;
margin-bottom: 2px;
@@ -71,12 +82,12 @@ body {
border-color: transparent !important;
}
#header button.btn-sm .bx {
#toolbar button.btn-sm .bx {
position: relative;
top: 1px;
}
#header button:hover {
#toolbar button:hover {
border-color: var(--button-border-color) !important;
}
@@ -90,7 +101,7 @@ body {
padding: 3px 0 3px 0;
border: 1px solid var(--main-border-color);
border-radius: 7px;
margin: 5px 15px 5px 5px;
margin: 3px 5px 5px 5px;
}
.dropdown-menu {
@@ -191,7 +202,7 @@ body {
.note-title-row {
flex-grow: 0;
flex-shrink: 0;
margin-top: 10px;
padding-top: 2px;
}
.note-title {
@@ -206,12 +217,13 @@ body {
.note-tab-row {
box-sizing: border-box;
position: relative;
height: 33px;
height: 34px;
min-height: 34px;
width: 100%;
background: var(--main-background-color);
border-radius: 5px 5px 0 0;
overflow: hidden;
grid-area: tabs;
margin-top: 5px;
margin-top: 2px;
}
.note-tab-row * {
box-sizing: inherit;
@@ -244,7 +256,7 @@ body {
text-align: center;
font-size: 24px;
cursor: pointer;
border-bottom: 1px solid var(--button-border-color);
border-bottom: 1px solid var(--main-border-color);
}
.note-new-tab:hover {
@@ -253,9 +265,15 @@ body {
}
.tab-row-filler {
-webkit-app-region: drag;
position: absolute;
left: 0;
background: linear-gradient(to right, var(--button-border-color), transparent);
height: 33px;
}
.tab-row-filler .tab-row-border {
position: relative;
background: linear-gradient(to right, var(--main-border-color), transparent);
height: 1px;
margin-top: 32px;
}
@@ -288,12 +306,12 @@ body {
overflow: hidden;
pointer-events: all;
background-color: var(--accented-background-color);
border-bottom: 1px solid var(--button-border-color);
border-bottom: 1px solid var(--main-border-color);
}
.note-tab-row .note-tab[active] .note-tab-wrapper {
background-color: var(--main-background-color);
border: 1px solid var(--button-border-color);
border: 1px solid var(--main-border-color);
border-bottom: 0;
font-weight: bold;
}
@@ -385,30 +403,26 @@ body {
transition: transform 120ms ease-in-out;
}
.hide-sidebar-button {
color: var(--main-text-color);
background: none;
border: 1px solid transparent;
padding: 2px 8px 2px 8px;
border-radius: 2px;
#hide-sidebar-button, #show-sidebar-button {
position: fixed;
bottom: 10px;
right: 10px;
z-index: 100000;
}
.hide-sidebar-button:hover {
border-color: var(--button-border-color);
}
.note-detail-sidebar {
#right-pane {
overflow: auto;
padding-top: 12px;
padding-top: 4px;
padding-left: 7px;
font-size: 90%;
height: 100%;
}
.note-detail-sidebar .card {
#right-pane .card {
border: 0;
}
.note-detail-sidebar .card-header {
#right-pane .card-header {
background: inherit;
padding: 5px 10px 5px 10px;
width: 99%; /* to give minimal right margin */
@@ -421,7 +435,7 @@ body {
justify-content: space-between;
}
.note-detail-sidebar .widget-title {
#right-pane .widget-title {
border-radius: 0;
padding: 0;
border: 0;
@@ -431,29 +445,29 @@ body {
color: var(--muted-text-color) !important;
}
.note-detail-sidebar .widget-header-action {
#right-pane .widget-header-action {
color: var(--link-color) !important;
cursor: pointer;
}
.note-detail-sidebar .widget-help {
#right-pane .widget-help {
color: var(--muted-text-color);
position: relative;
top: 2px;
}
.note-detail-sidebar .widget-help.no-link:hover {
#right-pane .widget-help.no-link:hover {
cursor: default;
text-decoration: none;
}
.note-detail-sidebar .card-body {
#right-pane .card-body {
width: 100%;
padding: 8px;
border: 0;
}
.note-detail-sidebar .card-body ul {
#right-pane .card-body ul {
padding-left: 25px;
margin-bottom: 5px;
}
@@ -485,4 +499,8 @@ body {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#title-bar-buttons {
min-width: 100px;
}
+39 -1
View File
@@ -105,6 +105,7 @@ span.fancytree-node.muted { opacity: 0.6; }
flex-direction: column;
flex-grow: 100;
height: 100%;
width: 100%;
}
.note-detail-component-wrapper {
@@ -159,6 +160,10 @@ span.fancytree-node.muted { opacity: 0.6; }
background-position: center;
}
.zen-mode #center-pane {
width: 100% !important;
}
ul.fancytree-container {
outline: none !important;
background-color: inherit !important;
@@ -333,7 +338,7 @@ div.ui-tooltip {
color: #888 !important;
}
.dropdown-menu a:hover:not(.disabled), li.dropdown-item:hover:not(.disabled) {
.dropdown-menu a:hover:not(.disabled), li.dropdown-item:hover:not(.disabled), div.dropdown-item:hover:not(.disabled) {
color: var(--hover-item-text-color) !important;
background-color: var(--hover-item-background-color) !important;
cursor: pointer;
@@ -435,6 +440,12 @@ div.ui-tooltip {
color: var(--primary-button-text-color);
}
.btn:not(:disabled):not(.disabled):active, .btn:not(:disabled):not(.disabled).active {
border-color: var(--primary-button-text-color);
background-color: var(--active-item-background-color);
color: var(--active-item-text-color);
}
.btn.btn-primary kbd {
color: var(--primary-button-text-color);
}
@@ -468,6 +479,17 @@ button.icon-button {
width: 15em;
}
#global-menu-wrapper {
height: 35px;
border-bottom: 1px solid var(--main-border-color);
}
#global-menu button {
margin-right: 10px;
height: 33px;
border-bottom: none;
}
#global-menu .dropdown-menu {
width: 20em;
}
@@ -929,4 +951,20 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
font-weight: bold !important;
color: inherit !important;
vertical-align: baseline !important;
}
.ck-content .todo-list .todo-list__label > input:before {
border: 1px solid var(--muted-text-color) !important;
}
.ck-link-actions .ck-tooltip {
/* force hide the tooltip since it shows misleading "open link in new tab */
display: none !important;
}
.include-note {
margin: 20px;
padding: 20px;
border-radius: 10px;
background-color: var(--accented-background-color);
}
+5 -7
View File
@@ -13,17 +13,11 @@ const ALLOWED_OPTIONS = new Set([
'syncServerHost',
'syncServerTimeout',
'syncProxy',
'leftPaneMinWidth',
'leftPaneWidthPercent',
'sidebarMinWidth',
'sidebarWidthPercent',
'showSidebarInNewTab',
'hoistedNoteId',
'mainFontSize',
'treeFontSize',
'detailFontSize',
'openTabs',
'hideTabRowForOneTab',
'noteInfoWidget',
'attributesWidget',
'linkMapWidget',
@@ -36,7 +30,11 @@ const ALLOWED_OPTIONS = new Set([
'spellCheckEnabled',
'spellCheckLanguageCode',
'imageMaxWidthHeight',
'imageJpegQuality'
'imageJpegQuality',
'leftPaneWidth',
'rightPaneWidth',
'rightPaneVisible',
'nativeTitleBarVisible'
]);
async function getOptions() {
+14 -2
View File
@@ -10,6 +10,8 @@ const contentHashService = require('../../services/content_hash');
const log = require('../../services/log');
const syncOptions = require('../../services/sync_options');
const dateUtils = require('../../services/date_utils');
const entityConstructor = require('../../entities/entity_constructor');
const utils = require('../../services/utils');
async function testSync() {
try {
@@ -47,7 +49,7 @@ async function getStats() {
async function checkSync() {
return {
hashes: await contentHashService.getHashes(),
entityHashes: await contentHashService.getEntityHashes(),
maxSyncId: await sql.getValue('SELECT MAX(id) FROM sync')
};
}
@@ -137,6 +139,15 @@ async function syncFinished() {
await sqlInit.dbInitialized();
}
async function queueSector(req) {
const entityName = utils.sanitizeSqlIdentifier(req.params.entityName);
const sector = utils.sanitizeSqlIdentifier(req.params.sector);
const entityPrimaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName;
await syncTableService.addEntitySyncsForSector(entityName, entityPrimaryKey, sector);
}
module.exports = {
testSync,
checkSync,
@@ -147,5 +158,6 @@ module.exports = {
getChanged,
update,
getStats,
syncFinished
syncFinished,
queueSector
};
+3
View File
@@ -10,6 +10,8 @@ async function getNotesAndBranches(noteIds) {
noteIds = notes.map(n => n.noteId);
// joining child note to filter out not completely synchronised notes which would then cause errors later
// cannot do that with parent because of root note's 'none' parent
const branches = await sql.getManyRows(`
SELECT
branches.branchId,
@@ -19,6 +21,7 @@ async function getNotesAndBranches(noteIds) {
branches.prefix,
branches.isExpanded
FROM branches
JOIN notes AS child ON child.noteId = branches.noteId
WHERE branches.isDeleted = 0
AND (branches.noteId IN (???) OR parentNoteId IN (???))`, noteIds);
+3 -7
View File
@@ -6,6 +6,7 @@ const attributeService = require('../services/attributes');
const config = require('../services/config');
const optionService = require('../services/options');
const log = require('../services/log');
const env = require('../services/env');
async function index(req, res) {
const options = await optionService.getOptionsMap();
@@ -18,19 +19,14 @@ async function index(req, res) {
res.render(view, {
csrfToken: csrfToken,
theme: options.theme,
leftPaneMinWidth: parseInt(options.leftPaneMinWidth),
leftPaneWidthPercent: parseInt(options.leftPaneWidthPercent),
rightPaneWidthPercent: 100 - parseInt(options.leftPaneWidthPercent),
sidebarMinWidth: parseInt(options.sidebarMinWidth),
sidebarWidthPercent: parseInt(options.sidebarWidthPercent),
contentWidthPercent: 100 - parseInt(options.sidebarWidthPercent),
mainFontSize: parseInt(options.mainFontSize),
treeFontSize: parseInt(options.treeFontSize),
detailFontSize: parseInt(options.detailFontSize),
sourceId: await sourceIdService.generateSourceId(),
maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync"),
instanceName: config.General ? config.General.instanceName : null,
appCssNoteIds: await getAppCssNoteIds()
appCssNoteIds: await getAppCssNoteIds(),
isDev: env.isDev()
});
}
+2 -1
View File
@@ -114,7 +114,7 @@ function register(app) {
route(GET, '/login', [auth.checkAppInitialized], loginRoute.loginPage);
route(POST, '/login', [], loginRoute.login);
route(POST, '/logout', [csrfMiddleware, auth.checkAuth], loginRoute.logout);
route(GET, '/setup', [auth.checkAppNotInitialized], setupRoute.setupPage);
route(GET, '/setup', [], setupRoute.setupPage);
apiRoute(GET, '/api/tree', treeApiRoute.getTree);
apiRoute(POST, '/api/tree/load', treeApiRoute.load);
@@ -199,6 +199,7 @@ function register(app) {
route(GET, '/api/sync/changed', [auth.checkApiAuth], syncApiRoute.getChanged, apiResultHandler);
route(PUT, '/api/sync/update', [auth.checkApiAuth], syncApiRoute.update, apiResultHandler);
route(POST, '/api/sync/finished', [auth.checkApiAuth], syncApiRoute.syncFinished, apiResultHandler);
route(POST, '/api/sync/queue-sector/:entityName/:sector', [auth.checkApiAuth], syncApiRoute.queueSector, apiResultHandler);
route(GET, '/api/sync/stats', [], syncApiRoute.getStats, apiResultHandler);
apiRoute(POST, '/api/recent-notes', recentNotesRoute.addRecentNote);
+9 -1
View File
@@ -2,10 +2,18 @@
const sqlInit = require('../services/sql_init');
const setupService = require('../services/setup');
const utils = require('../services/utils');
const windowService = require('../services/window');
async function setupPage(req, res) {
if (await sqlInit.isDbInitialized()) {
res.redirect('/');
if (utils.isElectron()) {
await windowService.createMainWindow();
windowService.closeSetupWindow();
}
else {
res.redirect('/');
}
}
// we got here because DB is not completely initialized so if schema exists
+1 -1
View File
@@ -5,7 +5,7 @@ const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir');
const APP_DB_VERSION = 155;
const SYNC_VERSION = 12;
const SYNC_VERSION = 13;
const CLIPPER_PROTOCOL_VERSION = "1.0";
module.exports = {
+1 -1
View File
@@ -83,7 +83,7 @@ async function checkBasicAuth(req, res, next) {
const dbUsername = await optionService.getOption('username');
if (dbUsername !== username || !await passwordEncryptionService.verifyPassword(password)) {
res.status(401).send("Not authorized");
res.status(401).send('Incorrect username and/or password');
}
else {
next();
+1 -1
View File
@@ -1 +1 @@
module.exports = { buildDate:"2019-12-10T23:09:02+01:00", buildRevision: "76f5736255251afe0e94b07236dc5c183384242f" };
module.exports = { buildDate:"2019-12-30T19:48:54+01:00", buildRevision: "3f1d0e5872d5d9185a3c24b92bcb080d70bcae60" };
+39 -32
View File
@@ -3,7 +3,6 @@
const sql = require('./sql');
const utils = require('./utils');
const log = require('./log');
const ws = require('./ws.js');
const ApiToken = require('../entities/api_token');
const Branch = require('../entities/branch');
const Note = require('../entities/note');
@@ -12,65 +11,73 @@ const NoteRevision = require('../entities/note_revision');
const RecentNote = require('../entities/recent_note');
const Option = require('../entities/option');
async function getHash(tableName, primaryKeyName, whereBranch) {
// subselect is necessary to have correct ordering in GROUP_CONCAT
const query = `SELECT GROUP_CONCAT(hash) FROM (SELECT hash FROM ${tableName} `
+ (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${primaryKeyName})`;
async function getSectorHashes(tableName, primaryKeyName, whereBranch) {
const hashes = await sql.getRows(`SELECT ${primaryKeyName} AS id, hash FROM ${tableName} `
+ (whereBranch ? `WHERE ${whereBranch} ` : '')
+ ` ORDER BY ${primaryKeyName}`);
let contentToHash = await sql.getValue(query);
const map = {};
if (!contentToHash) { // might be null in case of no rows
contentToHash = "";
for (const {id, hash} of hashes) {
map[id[0]] = (map[id[0]] || "") + hash;
}
return utils.hash(contentToHash);
for (const key in map) {
map[key] = utils.hash(map[key]);
}
return map;
}
async function getHashes() {
async function getEntityHashes() {
const startTime = new Date();
const hashes = {
notes: await getHash(Note.entityName, Note.primaryKeyName),
note_contents: await getHash("note_contents", "noteId"),
branches: await getHash(Branch.entityName, Branch.primaryKeyName),
note_revisions: await getHash(NoteRevision.entityName, NoteRevision.primaryKeyName),
note_revision_contents: await getHash("note_revision_contents", "noteRevisionId"),
recent_notes: await getHash(RecentNote.entityName, RecentNote.primaryKeyName),
options: await getHash(Option.entityName, Option.primaryKeyName, "isSynced = 1"),
attributes: await getHash(Attribute.entityName, Attribute.primaryKeyName),
api_tokens: await getHash(ApiToken.entityName, ApiToken.primaryKeyName),
notes: await getSectorHashes(Note.entityName, Note.primaryKeyName),
note_contents: await getSectorHashes("note_contents", "noteId"),
branches: await getSectorHashes(Branch.entityName, Branch.primaryKeyName),
note_revisions: await getSectorHashes(NoteRevision.entityName, NoteRevision.primaryKeyName),
note_revision_contents: await getSectorHashes("note_revision_contents", "noteRevisionId"),
recent_notes: await getSectorHashes(RecentNote.entityName, RecentNote.primaryKeyName),
options: await getSectorHashes(Option.entityName, Option.primaryKeyName, "isSynced = 1"),
attributes: await getSectorHashes(Attribute.entityName, Attribute.primaryKeyName),
api_tokens: await getSectorHashes(ApiToken.entityName, ApiToken.primaryKeyName),
};
const elapseTimeMs = Date.now() - startTime.getTime();
const elapsedTimeMs = Date.now() - startTime.getTime();
log.info(`Content hash computation took ${elapseTimeMs}ms`);
log.info(`Content hash computation took ${elapsedTimeMs}ms`);
return hashes;
}
async function checkContentHashes(otherHashes) {
const hashes = await getHashes();
let allChecksPassed = true;
const entityHashes = await getEntityHashes();
const failedChecks = [];
for (const key in hashes) {
if (hashes[key] !== otherHashes[key]) {
allChecksPassed = false;
for (const entityName in entityHashes) {
const thisSectorHashes = entityHashes[entityName];
const otherSectorHashes = otherHashes[entityName];
log.info(`Content hash check for ${key} FAILED. Local is ${hashes[key]}, remote is ${otherHashes[key]}`);
const sectors = new Set(Object.keys(thisSectorHashes).concat(Object.keys(otherSectorHashes)));
if (key !== 'recent_notes') {
// let's not get alarmed about recent notes which get updated often and can cause failures in race conditions
ws.sendMessageToAllClients({type: 'sync-hash-check-failed'});
for (const sector of sectors) {
if (thisSectorHashes[sector] !== otherSectorHashes[sector]) {
log.info(`Content hash check for ${entityName} sector ${sector} FAILED. Local is ${thisSectorHashes[sector]}, remote is ${otherSectorHashes[sector]}`);
failedChecks.push({ entityName, sector });
}
}
}
if (allChecksPassed) {
if (failedChecks.length === 0) {
log.info("Content hash checks PASSED");
}
return failedChecks;
}
module.exports = {
getHashes,
getEntityHashes,
checkContentHashes
};
+48 -10
View File
@@ -2,6 +2,7 @@
const html = require('html');
const repository = require('../repository');
const dateUtils = require('../date_utils');
const tar = require('tar-stream');
const path = require('path');
const mimeTypes = require('mime-types');
@@ -10,6 +11,8 @@ const packageInfo = require('../../../package.json');
const utils = require('../utils');
const protectedSessionService = require('../protected_session');
const sanitize = require("sanitize-filename");
const fs = require("fs");
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
/**
* @param {TaskContext} taskContext
@@ -187,13 +190,13 @@ async function exportToTar(taskContext, branch, format, res) {
for (let i = 0; i < targetPath.length - 1; i++) {
const meta = noteIdToMeta[targetPath[i]];
url += meta.dirFileName + '/';
url += encodeURIComponent(meta.dirFileName) + '/';
}
const meta = noteIdToMeta[targetPath[targetPath.length - 1]];
// link can target note which is only "folder-note" and as such will not have a file in an export
url += meta.dataFileName || meta.dirFileName;
url += encodeURIComponent(meta.dataFileName || meta.dirFileName);
return url;
}
@@ -202,13 +205,13 @@ async function exportToTar(taskContext, branch, format, res) {
content = content.replace(/src="[^"]*api\/images\/([a-zA-Z0-9]+)\/[^"]*"/g, (match, targetNoteId) => {
const url = getTargetUrl(targetNoteId, noteMeta);
return url ? `src="${encodeURIComponent(url)}"` : match;
return url ? `src="${url}"` : match;
});
content = content.replace(/href="[^"]*#root[a-zA-Z0-9\/]*\/([a-zA-Z0-9]+)\/?"/g, (match, targetNoteId) => {
const url = getTargetUrl(targetNoteId, noteMeta);
return url ? `href="${encodeURIComponent(url)}"` : match;
return url ? `href="${url}"` : match;
});
return content;
@@ -223,8 +226,13 @@ async function exportToTar(taskContext, branch, format, res) {
if (noteMeta.format === 'html') {
if (!content.substr(0, 100).toLowerCase().includes("<html")) {
const cssUrl = "../".repeat(noteMeta.notePath.length - 1) + 'style.css';
content = `<html>
<head><meta charset="utf-8"></head>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="${cssUrl}">
</head>
<body>
<h1>${utils.escapeHtml(title)}</h1>
${content}
@@ -255,7 +263,7 @@ ${content}
if (noteMeta.isClone) {
const targetUrl = getTargetUrl(noteMeta.noteId, noteMeta);
let content = `<p>This is a clone of a note. Go to its <a href="${encodeURIComponent(targetUrl)}">primary location</a>.</p>`;
let content = `<p>This is a clone of a note. Go to its <a href="${targetUrl}">primary location</a>.</p>`;
content = prepareContent(noteMeta.title, content, noteMeta);
@@ -270,7 +278,11 @@ ${content}
if (noteMeta.dataFileName) {
const content = prepareContent(noteMeta.title, await note.getContent(), noteMeta);
pack.entry({name: filePathPrefix + noteMeta.dataFileName, size: content.length}, content);
pack.entry({
name: filePathPrefix + noteMeta.dataFileName,
size: content.length,
mtime: dateUtils.parseDateTime(note.utcDateModified)
}, content);
}
taskContext.increaseProgressCount();
@@ -278,7 +290,11 @@ ${content}
if (noteMeta.children && noteMeta.children.length > 0) {
const directoryPath = filePathPrefix + noteMeta.dirFileName;
pack.entry({name: directoryPath, type: 'directory'});
pack.entry({
name: directoryPath,
type: 'directory',
mtime: dateUtils.parseDateTime(note.utcDateModified)
});
for (const childMeta of noteMeta.children) {
await saveNote(childMeta, directoryPath + '/');
@@ -314,7 +330,15 @@ ${content}
return html + '</li>';
}
const fullHtml = '<html><head><meta charset="utf-8"></head><body><ul>' + saveNavigationInner(rootMeta) + '</ul></body></html>'
const fullHtml = `<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<ul>${saveNavigationInner(rootMeta)}</ul>
</body>
</html>`;
const prettyHtml = html.prettyPrint(fullHtml, {indent_size: 2});
pack.entry({name: navigationMeta.dataFileName, size: prettyHtml.length}, prettyHtml);
@@ -351,6 +375,12 @@ ${content}
pack.entry({name: indexMeta.dataFileName, size: fullHtml.length}, fullHtml);
}
function saveCss(rootMeta, cssMeta) {
const cssContent = fs.readFileSync(RESOURCE_DIR + '/libraries/ckeditor/ckeditor-content.css');
pack.entry({name: cssMeta.dataFileName, size: cssContent.length}, cssContent);
}
const existingFileNames = format === 'html' ? ['navigation', 'index'] : [];
const rootMeta = await getNoteMeta(branch, { notePath: [] }, existingFileNames);
@@ -360,7 +390,7 @@ ${content}
files: [ rootMeta ]
};
let navigationMeta, indexMeta;
let navigationMeta, indexMeta, cssMeta;
if (format === 'html') {
navigationMeta = {
@@ -376,6 +406,13 @@ ${content}
};
metaFile.files.push(indexMeta);
cssMeta = {
noImport: true,
dataFileName: "style.css"
};
metaFile.files.push(cssMeta);
}
for (const noteMeta of Object.values(noteIdToMeta)) {
@@ -397,6 +434,7 @@ ${content}
if (format === 'html') {
saveNavigation(rootMeta, navigationMeta);
saveIndex(rootMeta, indexMeta);
saveCss(rootMeta, cssMeta);
}
pack.finalize();
+3 -3
View File
@@ -274,6 +274,9 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
return /^(?:[a-z]+:)?\/\//i.test(url);
}
content = content.replace(/<html.*<body[^>]*>/gis, "");
content = content.replace(/<\/body>.*<\/html>/gis, "");
content = content.replace(/src="([^"]*)"/g, (match, url) => {
url = decodeURIComponent(url);
@@ -298,9 +301,6 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
return `href="#root/${targetNoteId}"`;
});
content = content.replace(/<html.*<body[^>]*>/gis, "");
content = content.replace(/<\/body>.*<\/html>/gis, "");
content = content.replace(/<h1>([^<]*)<\/h1>/gi, (match, text) => {
if (noteTitle.trim() === text.trim()) {
return ""; // remove whole H1 tag
+5 -3
View File
@@ -3,7 +3,6 @@ const sqlInit = require('./sql_init');
const optionService = require('./options');
const dateUtils = require('./date_utils');
const syncTableService = require('./sync_table');
const attributeService = require('./attributes');
const eventService = require('./events');
const repository = require('./repository');
const cls = require('../services/cls');
@@ -462,8 +461,11 @@ async function eraseDeletedNotes() {
return;
}
// it's better to not use repository for this because it will complain about saving protected notes
// out of protected session, also we don't want these changes to be synced (since they are done on all instances anyway)
// it's better to not use repository for this because:
// - it would complain about saving protected notes out of protected session
// - we don't want these changes to be synced (since they are done on all instances anyway)
// - we don't want change the hash since this erasing happens on each instance separately
// and changing the hash would fire up the sync errors temporarily
// setting contentLength to zero would serve no benefit and it leaves potentially useful trail
await sql.executeMany(`
+5 -8
View File
@@ -33,7 +33,6 @@ async function initNotSyncedOptions(initialized, startNotePath = 'root', opts =
notePath: startNotePath,
active: true,
sidebar: {
visible: true,
widgets: []
}
}
@@ -61,14 +60,9 @@ const defaultOptions = [
{ name: 'protectedSessionTimeout', value: '600', isSynced: true },
{ name: 'hoistedNoteId', value: 'root', isSynced: false },
{ name: 'zoomFactor', value: '1.0', isSynced: false },
{ name: 'leftPaneMinWidth', value: '350', isSynced: false },
{ name: 'leftPaneWidthPercent', value: '20', isSynced: false },
{ name: 'mainFontSize', value: '100', isSynced: false },
{ name: 'treeFontSize', value: '100', isSynced: false },
{ name: 'detailFontSize', value: '110', isSynced: false },
{ name: 'sidebarMinWidth', value: '350', isSynced: false },
{ name: 'sidebarWidthPercent', value: '25', isSynced: false },
{ name: 'showSidebarInNewTab', value: 'true', isSynced: false },
{ name: 'calendarWidget', value: '{"enabled":true,"expanded":true,"position":20}', isSynced: false },
{ name: 'editedNotesWidget', value: '{"enabled":true,"expanded":true,"position":50}', isSynced: false },
{ name: 'noteInfoWidget', value: '{"enabled":true,"expanded":true,"position":100}', isSynced: false },
@@ -79,11 +73,14 @@ const defaultOptions = [
{ name: 'similarNotesWidget', value: '{"enabled":true,"expanded":true,"position":600}', isSynced: false },
{ name: 'spellCheckEnabled', value: 'true', isSynced: false },
{ name: 'spellCheckLanguageCode', value: 'en-US', isSynced: false },
{ name: 'hideTabRowForOneTab', value: 'false', isSynced: false },
{ name: 'imageMaxWidthHeight', value: '1200', isSynced: true },
{ name: 'imageJpegQuality', value: '75', isSynced: true },
{ name: 'autoFixConsistencyIssues', value: 'true', isSynced: false },
{ name: 'codeNotesMimeTypes', value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-swift","text/xml","text/x-yaml"]', isSynced: true }
{ name: 'codeNotesMimeTypes', value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-swift","text/xml","text/x-yaml"]', isSynced: true },
{ name: 'leftPaneWidth', value: '25', isSynced: false },
{ name: 'rightPaneWidth', value: '25', isSynced: false },
{ name: 'rightPaneVisible', value: 'true', isSynced: false },
{ name: 'nativeTitleBarVisible', value: 'false', isSynced: false }
];
async function initStartupOptions() {
+1 -1
View File
@@ -1,6 +1,6 @@
const dayjs = require("dayjs");
const filterRegex = /(\b(AND|OR)\s+)?@(!?)([\p{L}_]+|"[^"]+")\s*((=|!=|<|<=|>|>=|!?\*=|!?=\*|!?\*=\*)\s*([\p{L}_/-]+|"[^"]+"))?/igu;
const filterRegex = /(\b(AND|OR)\s+)?@(!?)([\p{L}\p{Number}_]+|"[^"]+")\s*((=|!=|<|<=|>|>=|!?\*=|!?=\*|!?\*=\*)\s*(\S+|"[^"]+"))?/igu;
const smartValueRegex = /^(NOW|TODAY|WEEK|MONTH|YEAR) *([+\-] *\d+)?$/i;
function calculateSmartValue(v) {
+2 -1
View File
@@ -71,7 +71,8 @@ async function setupSyncFromSyncServer(syncServerHost, syncProxy, username, pass
'user': username,
'pass': password
},
proxy: syncProxy
proxy: syncProxy,
timeout: 30000 // seed request should not take long
});
if (resp.syncVersion !== appInfo.syncVersion) {
+28 -9
View File
@@ -15,6 +15,8 @@ const syncMutexService = require('./sync_mutex');
const cls = require('./cls');
const request = require('./request');
const ws = require('./ws');
const syncTableService = require('./sync_table');
const entityConstructor = require('../entities/entity_constructor');
let proxyToggle = true;
@@ -30,17 +32,22 @@ async function sync() {
return { success: false, message: 'Sync not configured' };
}
const syncContext = await login();
let continueSync = false;
await pushSync(syncContext);
do {
const syncContext = await login();
await pullSync(syncContext);
await pushSync(syncContext);
await pushSync(syncContext);
await pullSync(syncContext);
await syncFinished(syncContext);
await pushSync(syncContext);
await checkContentHash(syncContext);
await syncFinished(syncContext);
continueSync = await checkContentHash(syncContext);
}
while (continueSync);
return {
success: true
@@ -225,7 +232,7 @@ async function checkContentHash(syncContext) {
if (await getLastSyncedPull() < resp.maxSyncId) {
log.info("There are some outstanding pulls, skipping content check.");
return;
return true;
}
const notPushedSyncs = await sql.getValue("SELECT EXISTS(SELECT 1 FROM sync WHERE id > ?)", [await getLastSyncedPush()]);
@@ -233,10 +240,22 @@ async function checkContentHash(syncContext) {
if (notPushedSyncs) {
log.info(`There's ${notPushedSyncs} outstanding pushes, skipping content check.`);
return;
return true;
}
await contentHashService.checkContentHashes(resp.hashes);
const failedChecks = await contentHashService.checkContentHashes(resp.entityHashes);
for (const {entityName, sector} of failedChecks) {
const entityPrimaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName;
await syncTableService.addEntitySyncsForSector(entityName, entityPrimaryKey, sector);
await syncRequest(syncContext, 'POST', `/api/sync/queue-sector/${entityName}/${sector}`);
log.info(`Added sector ${sector} of ${entityName} to sync queue.`);
}
return failedChecks.length > 0;
}
async function syncRequest(syncContext, method, requestPath, body) {
+1 -1
View File
@@ -23,6 +23,6 @@ module.exports = {
// and we need to override it with config from config.ini
return !!syncServerHost && syncServerHost !== 'disabled';
},
getSyncTimeout: async () => parseInt(await get('syncServerTimeout')),
getSyncTimeout: async () => parseInt(await get('syncServerTimeout')) || 60000,
getSyncProxy: async () => await get('syncProxy')
};
+23 -8
View File
@@ -6,7 +6,7 @@ const cls = require('./cls');
let syncs = [];
async function addEntitySync(entityName, entityId, sourceId) {
async function insertEntitySync(entityName, entityId, sourceId) {
const sync = {
entityName: entityName,
entityId: entityId,
@@ -16,11 +16,25 @@ async function addEntitySync(entityName, entityId, sourceId) {
sync.id = await sql.replace("sync", sync);
return sync;
}
async function addEntitySync(entityName, entityId, sourceId) {
const sync = await insertEntitySync(entityName, entityId, sourceId);
syncs.push(sync);
setTimeout(() => require('./ws').sendPingToAllClients(), 50);
}
async function addEntitySyncsForSector(entityName, entityPrimaryKey, sector) {
const entityIds = await sql.getColumn(`SELECT ${entityPrimaryKey} FROM ${entityName} WHERE SUBSTR(${entityPrimaryKey}, 1, 1) = ?`, [sector]);
for (const entityId of entityIds) {
await insertEntitySync(entityName, entityId, 'content-check');
}
}
function getMaxSyncId() {
return syncs.length === 0 ? 0 : syncs[syncs.length - 1].id;
}
@@ -29,19 +43,19 @@ function getEntitySyncsNewerThan(syncId) {
return syncs.filter(s => s.id > syncId);
}
async function cleanupSyncRowsForMissingEntities(entityName, entityKey) {
async function cleanupSyncRowsForMissingEntities(entityName, entityPrimaryKey) {
await sql.execute(`
DELETE
FROM sync
WHERE sync.entityName = '${entityName}'
AND sync.entityId NOT IN (SELECT ${entityKey} FROM ${entityName})`);
AND sync.entityId NOT IN (SELECT ${entityPrimaryKey} FROM ${entityName})`);
}
async function fillSyncRows(entityName, entityKey, condition = '') {
async function fillSyncRows(entityName, entityPrimaryKey, condition = '') {
try {
await cleanupSyncRowsForMissingEntities(entityName, entityKey);
await cleanupSyncRowsForMissingEntities(entityName, entityPrimaryKey);
const entityIds = await sql.getColumn(`SELECT ${entityKey} FROM ${entityName}`
const entityIds = await sql.getColumn(`SELECT ${entityPrimaryKey} FROM ${entityName}`
+ (condition ? ` WHERE ${condition}` : ''));
let createdCount = 0;
@@ -69,7 +83,7 @@ async function fillSyncRows(entityName, entityKey, condition = '') {
catch (e) {
// this is to fix migration from 0.30 to 0.32, can be removed later
// see https://github.com/zadam/trilium/issues/557
log.error(`Filling sync rows failed for ${entityName} ${entityKey} with error "${e.message}", continuing`);
log.error(`Filling sync rows failed for ${entityName} ${entityPrimaryKey} with error "${e.message}", continuing`);
}
}
@@ -101,5 +115,6 @@ module.exports = {
addEntitySync,
fillAllSyncRows,
getEntitySyncsNewerThan,
getMaxSyncId
getMaxSyncId,
addEntitySyncsForSector
};
+74 -54
View File
@@ -50,135 +50,155 @@ async function updateEntity(sync, entity, sourceId) {
}
}
async function updateNote(entity, sourceId) {
const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]);
function shouldWeUpdateEntity(localEntity, remoteEntity) {
if (!localEntity) {
return true;
}
if (!origNote || origNote.utcDateModified < entity.utcDateModified) {
const localDate = localEntity.utcDateModified || localEntity.utcDateCreated;
const remoteDate = remoteEntity.utcDateModified || remoteEntity.utcDateCreated;
if (localDate < remoteDate) {
return true;
}
// this can happen in case of sync error when hashes are different but dates are the same - we should still update
if (localEntity.hash !== remoteEntity.hash && localDate === remoteDate) {
return true;
}
return false;
}
async function updateNote(remoteEntity, sourceId) {
const localEntity = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [remoteEntity.noteId]);
if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
await sql.transactional(async () => {
await sql.replace("notes", entity);
await sql.replace("notes", remoteEntity);
await syncTableService.addNoteSync(entity.noteId, sourceId);
await syncTableService.addNoteSync(remoteEntity.noteId, sourceId);
});
log.info("Update/sync note " + entity.noteId);
log.info("Update/sync note " + remoteEntity.noteId);
}
}
async function updateNoteContent(entity, sourceId) {
const origNoteContent = await sql.getRow("SELECT * FROM note_contents WHERE noteId = ?", [entity.noteId]);
async function updateNoteContent(remoteEntity, sourceId) {
const localEntity = await sql.getRow("SELECT * FROM note_contents WHERE noteId = ?", [remoteEntity.noteId]);
if (!origNoteContent || origNoteContent.utcDateModified < entity.utcDateModified) {
entity.content = entity.content === null ? null : Buffer.from(entity.content, 'base64');
if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
remoteEntity.content = remoteEntity.content === null ? null : Buffer.from(remoteEntity.content, 'base64');
await sql.transactional(async () => {
await sql.replace("note_contents", entity);
await sql.replace("note_contents", remoteEntity);
await syncTableService.addNoteContentSync(entity.noteId, sourceId);
await syncTableService.addNoteContentSync(remoteEntity.noteId, sourceId);
});
log.info("Update/sync note content for noteId=" + entity.noteId);
log.info("Update/sync note content for noteId=" + remoteEntity.noteId);
}
}
async function updateBranch(entity, sourceId) {
const orig = await sql.getRowOrNull("SELECT * FROM branches WHERE branchId = ?", [entity.branchId]);
async function updateBranch(remoteEntity, sourceId) {
const localEntity = await sql.getRowOrNull("SELECT * FROM branches WHERE branchId = ?", [remoteEntity.branchId]);
await sql.transactional(async () => {
if (orig === null || orig.utcDateModified < entity.utcDateModified) {
if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
// isExpanded is not synced unless it's a new branch instance
// otherwise in case of full new sync we'll get all branches (even root) collapsed.
if (orig) {
delete entity.isExpanded;
if (localEntity) {
delete remoteEntity.isExpanded;
}
await sql.replace('branches', entity);
await sql.replace('branches', remoteEntity);
await syncTableService.addBranchSync(entity.branchId, sourceId);
await syncTableService.addBranchSync(remoteEntity.branchId, sourceId);
log.info("Update/sync branch " + entity.branchId);
log.info("Update/sync branch " + remoteEntity.branchId);
}
});
}
async function updateNoteRevision(entity, sourceId) {
const orig = await sql.getRowOrNull("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [entity.noteRevisionId]);
async function updateNoteRevision(remoteEntity, sourceId) {
const localEntity = await sql.getRowOrNull("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [remoteEntity.noteRevisionId]);
await sql.transactional(async () => {
if (orig === null || orig.utcDateModified < entity.utcDateModified) {
await sql.replace('note_revisions', entity);
if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
await sql.replace('note_revisions', remoteEntity);
await syncTableService.addNoteRevisionSync(entity.noteRevisionId, sourceId);
await syncTableService.addNoteRevisionSync(remoteEntity.noteRevisionId, sourceId);
log.info("Update/sync note revision " + entity.noteRevisionId);
log.info("Update/sync note revision " + remoteEntity.noteRevisionId);
}
});
}
async function updateNoteRevisionContent(entity, sourceId) {
const orig = await sql.getRowOrNull("SELECT * FROM note_revision_contents WHERE noteRevisionId = ?", [entity.noteRevisionId]);
async function updateNoteRevisionContent(remoteEntity, sourceId) {
const localEntity = await sql.getRowOrNull("SELECT * FROM note_revision_contents WHERE noteRevisionId = ?", [remoteEntity.noteRevisionId]);
await sql.transactional(async () => {
if (orig === null || orig.utcDateModified < entity.utcDateModified) {
entity.content = entity.content === null ? null : Buffer.from(entity.content, 'base64');
if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
remoteEntity.content = remoteEntity.content === null ? null : Buffer.from(remoteEntity.content, 'base64');
await sql.replace('note_revision_contents', entity);
await sql.replace('note_revision_contents', remoteEntity);
await syncTableService.addNoteRevisionContentSync(entity.noteRevisionId, sourceId);
await syncTableService.addNoteRevisionContentSync(remoteEntity.noteRevisionId, sourceId);
log.info("Update/sync note revision content " + entity.noteRevisionId);
log.info("Update/sync note revision content " + remoteEntity.noteRevisionId);
}
});
}
async function updateNoteReordering(entityId, entity, sourceId) {
async function updateNoteReordering(entityId, remote, sourceId) {
await sql.transactional(async () => {
for (const key in entity) {
await sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity[key], key]);
for (const key in remote) {
await sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [remote[key], key]);
}
await syncTableService.addNoteReorderingSync(entityId, sourceId);
});
}
async function updateOptions(entity, sourceId) {
const orig = await sql.getRowOrNull("SELECT * FROM options WHERE name = ?", [entity.name]);
async function updateOptions(remoteEntity, sourceId) {
const localEntity = await sql.getRowOrNull("SELECT * FROM options WHERE name = ?", [remoteEntity.name]);
if (orig && !orig.isSynced) {
if (localEntity && !localEntity.isSynced) {
return;
}
await sql.transactional(async () => {
if (orig === null || orig.utcDateModified < entity.utcDateModified) {
await sql.replace('options', entity);
if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
await sql.replace('options', remoteEntity);
await syncTableService.addOptionsSync(entity.name, sourceId);
await syncTableService.addOptionsSync(remoteEntity.name, sourceId);
}
});
}
async function updateRecentNotes(entity, sourceId) {
const orig = await sql.getRowOrNull("SELECT * FROM recent_notes WHERE noteId = ?", [entity.noteId]);
async function updateRecentNotes(remoteEntity, sourceId) {
const localEntity = await sql.getRowOrNull("SELECT * FROM recent_notes WHERE noteId = ?", [remoteEntity.noteId]);
if (orig === null || orig.utcDateCreated < entity.utcDateCreated) {
if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
await sql.transactional(async () => {
await sql.replace('recent_notes', entity);
await sql.replace('recent_notes', remoteEntity);
await syncTableService.addRecentNoteSync(entity.noteId, sourceId);
await syncTableService.addRecentNoteSync(remoteEntity.noteId, sourceId);
});
}
}
async function updateAttribute(entity, sourceId) {
const origAttribute = await sql.getRow("SELECT * FROM attributes WHERE attributeId = ?", [entity.attributeId]);
async function updateAttribute(remoteEntity, sourceId) {
const localEntity = await sql.getRow("SELECT * FROM attributes WHERE attributeId = ?", [remoteEntity.attributeId]);
if (!origAttribute || origAttribute.utcDateModified <= entity.utcDateModified) {
if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
await sql.transactional(async () => {
await sql.replace("attributes", entity);
await sql.replace("attributes", remoteEntity);
await syncTableService.addAttributeSync(entity.attributeId, sourceId);
await syncTableService.addAttributeSync(remoteEntity.attributeId, sourceId);
});
log.info("Update/sync attribute " + entity.attributeId);
log.info("Update/sync attribute " + remoteEntity.attributeId);
}
}
+2 -1
View File
@@ -63,7 +63,8 @@ async function getNotes(noteIds) {
SELECT
noteId,
title,
isProtected,
contentLength,
isProtected,
type,
mime,
isDeleted
+5
View File
@@ -53,6 +53,10 @@ function sanitizeSql(str) {
return str.replace(/'/g, "''");
}
function sanitizeSqlIdentifier(str) {
return str.replace(/[^A-Za-z0-9_]/g, "");
}
function prepareSqlForLike(prefix, str, suffix) {
const value = str
.replace(/\\/g, "\\\\")
@@ -174,6 +178,7 @@ module.exports = {
hash,
isEmptyOrWhitespace,
sanitizeSql,
sanitizeSqlIdentifier,
prepareSqlForLike,
stopWatch,
escapeHtml,
+131
View File
@@ -0,0 +1,131 @@
const path = require('path');
const url = require("url");
const port = require('./port');
const optionService = require('./options');
const env = require('./env');
const log = require('./log');
const sqlInit = require('./sql_init');
const cls = require('./cls');
const keyboardActionsService = require('./keyboard_actions');
// Prevent window being garbage collected
/** @type {Electron.BrowserWindow} */
let mainWindow;
/** @type {Electron.BrowserWindow} */
let setupWindow;
async function createMainWindow() {
const windowStateKeeper = require('electron-window-state'); // should not be statically imported
const mainWindowState = windowStateKeeper({
// default window width & height so it's usable on 1600 * 900 display (including some extra panels etc.)
defaultWidth: 1200,
defaultHeight: 800
});
const {BrowserWindow} = require('electron'); // should not be statically imported
mainWindow = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
title: 'Trilium Notes',
webPreferences: {
nodeIntegration: true
},
frame: await optionService.getOptionBool('nativeTitleBarVisible'),
icon: getIcon()
});
mainWindowState.manage(mainWindow);
mainWindow.setMenuBarVisibility(false);
mainWindow.loadURL('http://127.0.0.1:' + await port);
mainWindow.on('closed', () => mainWindow = null);
mainWindow.webContents.on('new-window', (e, url) => {
if (url !== mainWindow.webContents.getURL()) {
e.preventDefault();
require('electron').shell.openExternal(url);
}
});
// prevent drag & drop to navigate away from trilium
mainWindow.webContents.on('will-navigate', (ev, targetUrl) => {
const parsedUrl = url.parse(targetUrl);
// we still need to allow internal redirects from setup and migration pages
if (!['localhost', '127.0.0.1'].includes(parsedUrl.hostname) || (parsedUrl.path && parsedUrl.path !== '/')) {
ev.preventDefault();
}
});
}
function getIcon() {
return path.join(__dirname, '../../images/app-icons/png/256x256' + (env.isDev() ? '-dev' : '') + '.png');
}
async function createSetupWindow() {
const {BrowserWindow} = require('electron'); // should not be statically imported
setupWindow = new BrowserWindow({
width: 800,
height: 800,
title: 'Trilium Notes Setup',
icon: getIcon(),
webPreferences: {
// necessary for e.g. utils.isElectron()
nodeIntegration: true
}
});
setupWindow.setMenuBarVisibility(false);
setupWindow.loadURL('http://127.0.0.1:' + await port);
setupWindow.on('closed', () => setupWindow = null);
}
function closeSetupWindow() {
if (setupWindow) {
setupWindow.close();
}
}
async function registerGlobalShortcuts() {
const {globalShortcut} = require('electron');
await sqlInit.dbReady;
const allActions = await keyboardActionsService.getKeyboardActions();
for (const action of allActions) {
if (!action.effectiveShortcuts) {
continue;
}
for (const shortcut of action.effectiveShortcuts) {
if (shortcut.startsWith('global:')) {
const translatedShortcut = shortcut.substr(7);
const result = globalShortcut.register(translatedShortcut, cls.wrap(async () => {
// window may be hidden / not in focus
mainWindow.focus();
mainWindow.webContents.send('globalShortcut', action.actionName);
}));
if (result) {
log.info(`Registered global shortcut ${translatedShortcut} for action ${action.actionName}`);
}
else {
log.info(`Could not register global shortcut ${translatedShortcut}`);
}
}
}
}
}
module.exports = {
createMainWindow,
createSetupWindow,
closeSetupWindow,
registerGlobalShortcuts
};
+39
View File
@@ -0,0 +1,39 @@
<div id="center-pane">
<div id="note-tab-container">
<div class="note-tab-content note-tab-content-template">
<div class="note-detail-content">
<% include title.ejs %>
<div class="note-detail-script-area"></div>
<table class="note-detail-promoted-attributes"></table>
<div class="note-detail-component-wrapper">
<div class="note-detail-text note-detail-component">
<div class="note-detail-text-editor" tabindex="10000"></div>
</div>
<div class="note-detail-code note-detail-component">
<div class="note-detail-code-editor"></div>
</div>
<% include details/empty.ejs %>
<% include details/search.ejs %>
<% include details/render.ejs %>
<% include details/file.ejs %>
<% include details/image.ejs %>
<% include details/relation_map.ejs %>
<% include details/protected_session_password.ejs %>
<% include details/book.ejs %>
</div>
</div>
</div>
</div>
</div>
+96 -76
View File
@@ -10,48 +10,9 @@
<div id="toast-container" class="d-flex flex-column justify-content-center align-items-center"></div>
<div id="container" style="display: none; grid-template-columns: minmax(<%= leftPaneMinWidth %>px, <%= leftPaneWidthPercent %>fr) minmax(0, <%= rightPaneWidthPercent %>fr)">
<div id="header" class="hide-in-zen-mode">
<div id="history-navigation" style="display: none;">
<a id="history-back-button" title="Go to previous note." class="icon-action bx bx-left-arrow-circle"></a>
<a id="history-forward-button" title="Go to next note." class="icon-action bx bx-right-arrow-circle"></a>
</div>
<div style="flex-grow: 100; display: flex;">
<button class="btn btn-sm" id="jump-to-note-dialog-button" data-kb-action="JumpToNote">
<span class="bx bx-crosshair"></span>
Jump to note
</button>
<button class="btn btn-sm" id="recent-changes-button" data-kb-action="ShowRecentChanges">
<span class="bx bx-history"></span>
Recent changes
</button>
<button class="btn btn-sm"
id="enter-protected-session-button"
title="Enter protected session to be able to find and view protected notes">
<span class="bx bx-log-in"></span>
Enter protected session
</button>
<button class="btn btn-sm"
id="leave-protected-session-button"
title="Leave protected session so that protected notes are not accessible any more."
style="display: none;">
<span class="bx bx-log-out"></span>
Leave protected session
</button>
</div>
<div id="plugin-buttons">
</div>
<div>
<div id="container" style="display: none;">
<div id="topbar">
<div id="global-menu-wrapper">
<div class="dropdown" id="global-menu">
<button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
<span class="bx bx-menu"></span>
@@ -123,54 +84,109 @@
</div>
</div>
</div>
</div>
<div id="left-pane" class="hide-in-zen-mode">
<div id="global-buttons">
<a id="create-top-level-note-button" title="Create new top level note" class="icon-action bx bx-folder-plus"></a>
<a id="collapse-tree-button" title="Collapse note tree" data-kb-action="CollapseTree" class="icon-action bx bx-layer-minus"></a>
<a id="scroll-to-active-note-button" title="Scroll to active note" data-kb-action="ScrollToActiveNote" class="icon-action bx bx-crosshair"></a>
<a id="toggle-search-button" title="Search in notes" data-kb-action="SearchNotes" class="icon-action bx bx-search"></a>
<div class="note-tab-row">
<div class="note-tab-row-content"></div>
</div>
<div id="search-box">
<div class="form-group">
<div class="input-group">
<input name="search-text" id="search-text" class="form-control"
placeholder="Search text, labels" autocomplete="off">
<div id="title-bar-buttons" style="display: none;">
<button class="btn icon-action bx bx-minus" id="minimize-btn"></button>
<button class="btn icon-action bx bx-checkbox" id="maximize-btn"></button>
<button class="btn icon-action bx bx-x" id="close-btn"></button>
</div>
</div>
<div class="input-group-append">
<button id="do-search-button" class="btn btn-sm icon-button bx bx-search" title="Search (enter)"></button>
<div id="toolbar" class="hide-in-zen-mode">
<div id="history-navigation" style="display: none;">
<a id="history-back-button" title="Go to previous note." class="icon-action bx bx-left-arrow-circle"></a>
<a id="history-forward-button" title="Go to next note." class="icon-action bx bx-right-arrow-circle"></a>
</div>
<div style="flex-grow: 100; display: flex;">
<button class="btn btn-sm" id="jump-to-note-dialog-button" data-kb-action="JumpToNote">
<span class="bx bx-crosshair"></span>
Jump to note
</button>
<button class="btn btn-sm" id="recent-changes-button" data-kb-action="ShowRecentChanges">
<span class="bx bx-history"></span>
Recent changes
</button>
<button class="btn btn-sm"
id="enter-protected-session-button"
title="Enter protected session to be able to find and view protected notes">
<span class="bx bx-log-in"></span>
Enter protected session
</button>
<button class="btn btn-sm"
id="leave-protected-session-button"
title="Leave protected session so that protected notes are not accessible any more."
style="display: none;">
<span class="bx bx-log-out"></span>
Leave protected session
</button>
</div>
<div id="plugin-buttons">
</div>
</div>
<div style="display: flex; height: 100%;">
<div id="left-pane" class="hide-in-zen-mode">
<div id="global-buttons">
<a id="create-top-level-note-button" title="Create new top level note" class="icon-action bx bx-folder-plus"></a>
<a id="collapse-tree-button" title="Collapse note tree" data-kb-action="CollapseTree" class="icon-action bx bx-layer-minus"></a>
<a id="scroll-to-active-note-button" title="Scroll to active note" data-kb-action="ScrollToActiveNote" class="icon-action bx bx-crosshair"></a>
<a id="toggle-search-button" title="Search in notes" data-kb-action="SearchNotes" class="icon-action bx bx-search"></a>
</div>
<div id="search-box">
<div class="form-group">
<div class="input-group">
<input name="search-text" id="search-text" class="form-control"
placeholder="Search text, labels" autocomplete="off">
<div class="input-group-append">
<button id="do-search-button" class="btn btn-sm icon-button bx bx-search" title="Search (enter)"></button>
</div>
</div>
</div>
<div style="display: flex; align-items: center; justify-content: space-evenly; flex-wrap: wrap;">
<button id="save-search-button" class="btn btn-sm"
title="This will create new saved search note under active note.">
<span class="bx bx-save"></span> Save search</button>
<button id="close-search-button" class="btn btn-sm"><span class="bx bx-x"></span> Close search</button>
</div>
</div>
<div id="search-results">
<strong>Search results:</strong>
<div style="display: flex; align-items: center; justify-content: space-evenly; flex-wrap: wrap;">
<button id="save-search-button" class="btn btn-sm"
title="This will create new saved search note under active note.">
<span class="bx bx-save"></span> Save search</button>
<button id="close-search-button" class="btn btn-sm"><span class="bx bx-x"></span> Close search</button>
<ul id="search-results-inner"></ul>
</div>
<div id="tree"></div>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div>
</div>
<div id="search-results">
<strong>Search results:</strong>
<% include center.ejs %>
<ul id="search-results-inner"></ul>
</div>
<div id="tree"></div>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div>
<% include sidebar.ejs %>
</div>
<% include tabs.ejs %>
<% include dialogs/about.ejs %>
<% include dialogs/add_link.ejs %>
<% include dialogs/attributes.ejs %>
@@ -194,6 +210,7 @@
<% include dialogs/clone_to.ejs %>
<% include dialogs/move_to.ejs %>
<% include dialogs/backend_log.ejs %>
<% include dialogs/include_note.ejs %>
</div>
<script type="text/javascript">
@@ -204,7 +221,8 @@
sourceId: '<%= sourceId %>',
maxSyncIdAtLoad: <%= maxSyncIdAtLoad %>,
instanceName: '<%= instanceName %>',
csrfToken: '<%= csrfToken %>'
csrfToken: '<%= csrfToken %>',
isDev: '<%= isDev %>'
};
window.appCssNoteIds = <%- JSON.stringify(appCssNoteIds) %>;
</script>
@@ -228,6 +246,8 @@
<script src="libraries/dayjs.min.js"></script>
<script src="libraries/split.min.js"></script>
<link href="stylesheets/themes.css" rel="stylesheet">
<link href="stylesheets/style.css" rel="stylesheet">
<link href="stylesheets/desktop.css" rel="stylesheet">
+25
View File
@@ -0,0 +1,25 @@
<div id="include-note-dialog" class="modal mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Include note</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="include-note-form">
<div class="modal-body">
<div class="form-group">
<label for="include-note-autocomplete">Note</label>
<div class="input-group">
<input id="include-note-autocomplete" class="form-control" placeholder="search for note by its name">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Include note <kbd>enter</kbd></button>
</div>
</form>
</div>
</div>
</div>
+2 -1
View File
@@ -97,7 +97,8 @@
sourceId: '<%= sourceId %>',
maxSyncIdAtLoad: <%= maxSyncIdAtLoad %>,
instanceName: '<%= instanceName %>',
csrfToken: '<%= csrfToken %>'
csrfToken: '<%= csrfToken %>',
isDev: '<%= isDev %>'
};
</script>
+4 -5
View File
@@ -1,7 +1,6 @@
<div class="note-detail-sidebar hide-in-zen-mode" style="width: <%= sidebarWidthPercent %>%; min-width: <%= sidebarMinWidth %>px">
<div style="text-align: center; margin-bottom: 5px;">
<button class="hide-sidebar-button">hide sidebar <span class="bx bx-chevrons-right"></span></button>
</div>
<button id="hide-sidebar-button" class="btn btn-sm icon-button bx bx-chevrons-right" title="Hide sidebar"></button>
<button id="show-sidebar-button" class="btn btn-sm icon-button bx bx-chevrons-left" title="Show sidebar"></button>
<div class="note-detail-widget-container"></div>
<div id="right-pane" class="hide-in-zen-mode">
<div id="sidebar-container"></div>
</div>
-43
View File
@@ -1,43 +0,0 @@
<div class="note-tab-row hide-in-hide-in-zen-mode">
<div class="note-tab-row-content"></div>
</div>
<div id="note-tab-container">
<div class="note-tab-content note-tab-content-template">
<div class="note-detail-content" style="width: <%= contentWidthPercent %>%">
<% include title.ejs %>
<div class="note-detail-script-area"></div>
<table class="note-detail-promoted-attributes"></table>
<div class="note-detail-component-wrapper">
<div class="note-detail-text note-detail-component">
<div class="note-detail-text-editor" tabindex="10000"></div>
</div>
<div class="note-detail-code note-detail-component">
<div class="note-detail-code-editor"></div>
</div>
<% include details/empty.ejs %>
<% include details/search.ejs %>
<% include details/render.ejs %>
<% include details/file.ejs %>
<% include details/image.ejs %>
<% include details/relation_map.ejs %>
<% include details/protected_session_password.ejs %>
<% include details/book.ejs %>
</div>
</div>
<% include sidebar.ejs %>
</div>
</div>
-2
View File
@@ -65,8 +65,6 @@
<a class="dropdown-item show-note-info-button"><kbd data-kb-action="ShowNoteInfo"></kbd> Note info</a>
</div>
</div>
<button class="btn btn-sm icon-button bx bx-chevrons-left show-sidebar-button" title="Show sidebar"></button>
</div>
</div>
</div>