Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 631a75deec | |||
| 3f1d0e5872 | |||
| 0fe91d0184 | |||
| 2f711a12f8 | |||
| 64f32ba38f | |||
| 7db4859fb9 | |||
| eee9fcae5c | |||
| 9c4a976342 | |||
| e4a09c6207 | |||
| d467db2227 | |||
| b8d6ff0542 | |||
| a9b8e65c9b | |||
| bb8b563ece | |||
| 05a8ffb944 | |||
| 2502646a64 | |||
| 3d95d69f80 | |||
| df751f5d67 | |||
| 4f06b6de78 | |||
| d2177cd517 | |||
| 0affcf5ad2 | |||
| 7a416b107b | |||
| 1ff124dab7 | |||
| 4cb511bad0 | |||
| 73c8d145fa | |||
| ab79f24729 | |||
| cec71f65b3 | |||
| f75c008154 | |||
| 474baa7d95 | |||
| a155b6e8d5 | |||
| 229974e543 | |||
| 6fc19bfb93 | |||
| ccaa108faa | |||
| 0a72383495 | |||
| d389100611 | |||
| ea7257a5b2 | |||
| 0ebc947fbd | |||
| c89514f9bb | |||
| e0368e395c | |||
| 6986c201dd | |||
| bcf163f8a1 | |||
| 15aaead7b9 | |||
| d29c5c4758 | |||
| 81e2baeee5 | |||
| 4cececafc9 | |||
| 7c8e7a3f4b | |||
| 613d5f93e8 | |||
| 4f5b23fbf8 | |||
| a37b9cfc7b | |||
| 2bc18bc214 | |||
| f31a998c5d | |||
| 5552917533 | |||
| a9702aa6a2 | |||
| d1941cc650 | |||
| f98fa4098f | |||
| 5350496ed4 | |||
| b62d79044a | |||
| 0db3722ec2 | |||
| d47403c0e7 | |||
| 77311954a1 | |||
| b7cf4fe96b | |||
| 6d9b702d4c | |||
| 6e4c30571c | |||
| 5988776b7e | |||
| 384da60953 | |||
| 21fab412cb | |||
| eb4dfbad92 | |||
| aff9ce97ee | |||
| 882ebdbd8f |
+2
-2
@@ -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
@@ -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
@@ -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
Binary file not shown.
+15
-111
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+1
-1
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
Vendored
+3
File diff suppressed because one or more lines are too long
Generated
+149
-153
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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} */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(' ')
|
||||
.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(' ')
|
||||
.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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
@@ -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
@@ -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()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 @@
|
||||
module.exports = { buildDate:"2019-12-10T23:09:02+01:00", buildRevision: "76f5736255251afe0e94b07236dc5c183384242f" };
|
||||
module.exports = { buildDate:"2019-12-30T19:48:54+01:00", buildRevision: "3f1d0e5872d5d9185a3c24b92bcb080d70bcae60" };
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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')
|
||||
};
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,8 @@ async function getNotes(noteIds) {
|
||||
SELECT
|
||||
noteId,
|
||||
title,
|
||||
isProtected,
|
||||
contentLength,
|
||||
isProtected,
|
||||
type,
|
||||
mime,
|
||||
isDeleted
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
@@ -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">
|
||||
|
||||
@@ -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">×</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>
|
||||
@@ -97,7 +97,8 @@
|
||||
sourceId: '<%= sourceId %>',
|
||||
maxSyncIdAtLoad: <%= maxSyncIdAtLoad %>,
|
||||
instanceName: '<%= instanceName %>',
|
||||
csrfToken: '<%= csrfToken %>'
|
||||
csrfToken: '<%= csrfToken %>',
|
||||
isDev: '<%= isDev %>'
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user