Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b8509c487d | |||
| 083dee9bdd | |||
| 0c2dd6aa35 | |||
| 743c15aacc | |||
| 263bff903c | |||
| 741dfaf2de | |||
| b3834324f6 | |||
| ea0835561c | |||
| b0c0c5f56b | |||
| 6d5f8e0562 | |||
| 04633bdf3a | |||
| 97219aa12e | |||
| e825abf893 | |||
| 63b655cff4 | |||
| 0524942d11 | |||
| b5d75f183a | |||
| 706fc647ff | |||
| 13e9f9f9e7 | |||
| a76dcb44ae | |||
| b9373806cf | |||
| 9de2927304 | |||
| c3e1126489 | |||
| 3413c9ed64 | |||
| dcebcb0e73 | |||
| 3d7a5f20e7 | |||
| 6a99af64a8 | |||
| 7d57961ab2 | |||
| 95a773e5c9 | |||
| a912b2f23d | |||
| 36b581489c | |||
| 976684a3a8 | |||
| 093dfb4a39 | |||
| ddf381f92d | |||
| 2b44f3bc76 | |||
| 7b1fdfabf8 | |||
| 070e8d9647 | |||
| bf3360572a | |||
| e5036318af | |||
| 6d2394a9da | |||
| 427a266c57 | |||
| 196264b8c2 | |||
| afe24866f0 | |||
| d18a20cc06 | |||
| e94669de03 | |||
| 9c91b0459e | |||
| b161db064e | |||
| ec4abe0d81 | |||
| af21dd4463 | |||
| ef46727870 | |||
| 1ea0d283de | |||
| ed380e09c9 | |||
| b5daa83d69 | |||
| c4b957427d | |||
| 2f3b256272 | |||
| 6e3d8472e1 | |||
| 2a9f36a027 | |||
| cf3726289c | |||
| c8e4a5c9e7 | |||
| 3851bedb57 | |||
| a3951f1cce | |||
| 7c77ae758b | |||
| 20d3d61cec | |||
| 1b831f94a9 | |||
| 3f241553aa | |||
| 8624c83108 | |||
| d49cae9cea | |||
| 174128447b | |||
| 5d213eea7e | |||
| f45e25172b | |||
| ec87856ef4 | |||
| 6feb7ad1d5 | |||
| 6833e84d55 | |||
| 154a575701 | |||
| 504b2e2c4a | |||
| 2c5292222f | |||
| 989da8877b | |||
| 70591eadd5 | |||
| fead3cd7ad | |||
| a7cf3cdf05 | |||
| 5372f71faa | |||
| 3b76939f17 | |||
| c827d38f39 | |||
| 1e7586fae4 | |||
| ee89111bfb | |||
| 288b3077b2 | |||
| 5c1922ac9f | |||
| 93f1d46104 | |||
| c5daaf541a | |||
| 12b17b1e9e | |||
| ad46ba7d0f | |||
| 44f592c6cb | |||
| 943b35fb7d | |||
| f1967fd466 | |||
| ce2404b18a |
Generated
+5
@@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/workspace.xml
|
||||
|
||||
# Datasource local storage ignored files
|
||||
/dataSources.local.xml
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
FROM node:12.4.0-alpine
|
||||
FROM node:12.6.0-alpine
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
@@ -18,6 +18,7 @@ RUN set -x \
|
||||
make \
|
||||
nasm \
|
||||
libpng-dev \
|
||||
python \
|
||||
&& npm install --production \
|
||||
&& apk del .build-dependencies
|
||||
|
||||
|
||||
@@ -15,16 +15,17 @@ Trilium Notes is a hierarchical note taking application with focus on building l
|
||||
* Note [attributes](https://github.com/zadam/trilium/wiki/Attributes) can be used for note organization, querying and advanced [scripting](https://github.com/zadam/trilium/wiki/Scripts)
|
||||
* [Synchronization](https://github.com/zadam/trilium/wiki/Synchronization) with self-hosted sync server
|
||||
* Strong [note encryption](https://github.com/zadam/trilium/wiki/Protected-notes) with per-note granularity
|
||||
* [Relation maps](https://github.com/zadam/trilium/wiki/Relation-map) for visualizing notes and their relations
|
||||
* [Relation maps](https://github.com/zadam/trilium/wiki/Relation-map) and [link maps](https://github.com/zadam/trilium/wiki/Link-map) for visualizing notes and their relations
|
||||
* [Scripting](https://github.com/zadam/trilium/wiki/Scripts) - see [Advanced showcases](https://github.com/zadam/trilium/wiki/Advanced-showcases)
|
||||
* Scales well in both usability and performance upwards of 100 000 notes
|
||||
* Touch optimized [mobile frontend](https://github.com/zadam/trilium/wiki/Mobile-frontend) for smartphones and tablets
|
||||
* [Night theme](https://github.com/zadam/trilium/wiki/Themes)
|
||||
* [Evernote](https://github.com/zadam/trilium/wiki/Evernote-import) and [Markdown import & export](https://github.com/zadam/trilium/wiki/Markdown)
|
||||
* [Web Clipper](https://github.com/zadam/trilium/wiki/Web-clipper) for easy saving of web content
|
||||
|
||||
## Builds
|
||||
|
||||
Trilium is provided as either desktop application (Linux, Windows, Mac) or web application hosted on your server (Linux).
|
||||
Trilium is provided as either desktop application (Linux and Windows) or web application hosted on your server (Linux). Mac OS desktop build is available, but it is [unsupported](https://github.com/zadam/trilium/wiki/FAQ#mac-os-support).
|
||||
|
||||
* If you want to use Trilium on the desktop, download binary release for your platform from [latest release](https://github.com/zadam/trilium/releases/latest), unzip the package and run ```trilium``` executable.
|
||||
* If you want to install Trilium on server, follow [this page](https://github.com/zadam/trilium/wiki/Server-installation).
|
||||
|
||||
@@ -7,6 +7,9 @@ rm -r node_modules/sqlite3/lib/binding/*
|
||||
|
||||
cp -r bin/deps/linux-x64/sqlite/* node_modules/sqlite3/lib/binding/
|
||||
|
||||
# rebuild binaries for image operations (pngquant ...)
|
||||
npm rebuild
|
||||
|
||||
./node_modules/.bin/electron-packager . --asar --out=dist --executable-name=trilium --platform=linux --arch=x64 --overwrite
|
||||
|
||||
mv "./dist/Trilium Notes-linux-x64" $BUILD_DIR
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PKG_DIR=dist/trilium-linux-x64-server
|
||||
NODE_VERSION=12.4.0
|
||||
NODE_VERSION=12.6.0
|
||||
|
||||
rm -r $PKG_DIR
|
||||
mkdir $PKG_DIR
|
||||
@@ -30,11 +30,11 @@ rm -r ./node_modules/sqlite3/lib/binding/*
|
||||
|
||||
cp -r ../../bin/deps/linux-x64/sqlite/node* ./node_modules/sqlite3/lib/binding/
|
||||
|
||||
printf "#/bin/sh\n./node/bin/node src/www" > trilium.sh
|
||||
printf "#!/bin/sh\n./node/bin/node src/www" > trilium.sh
|
||||
chmod 755 trilium.sh
|
||||
|
||||
cd ..
|
||||
|
||||
VERSION=`jq -r ".version" ../package.json`
|
||||
|
||||
tar cJf trilium-linux-x64-server-${VERSION}.tar.xz trilium-linux-x64-server
|
||||
tar cJf trilium-linux-x64-server-${VERSION}.tar.xz trilium-linux-x64-server
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export GITHUB_REPO=trilium
|
||||
|
||||
if [[ $# -eq 0 ]] ; then
|
||||
echo "Missing argument of new version"
|
||||
exit 1
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
instanceName=
|
||||
|
||||
[Network]
|
||||
# host setting is relevant only for web deployments - set the host on which the server will listen
|
||||
# host=0.0.0.0
|
||||
# port setting is relevant only for web deployments, desktop builds run on random free port
|
||||
port=8080
|
||||
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).
|
||||
|
||||
+24
-3
@@ -9,6 +9,7 @@ const url = require("url");
|
||||
const port = require('./src/services/port');
|
||||
const appIconService = require('./src/services/app_icon');
|
||||
const windowStateKeeper = require('electron-window-state');
|
||||
const contextMenu = require('electron-context-menu');
|
||||
|
||||
const app = electron.app;
|
||||
const globalShortcut = electron.globalShortcut;
|
||||
@@ -23,6 +24,26 @@ let mainWindow;
|
||||
|
||||
require('electron-dl')({ saveAs: true });
|
||||
|
||||
contextMenu({
|
||||
menu: (actions, params, browserWindow) => [
|
||||
actions.cut(),
|
||||
actions.copy(),
|
||||
actions.copyLink(),
|
||||
actions.paste(),
|
||||
{
|
||||
label: 'Search DuckDuckGo for “{selection}”',
|
||||
// Only show it when right-clicking text
|
||||
visible: params.selectionText.trim().length > 0,
|
||||
click: () => {
|
||||
const {shell} = require('electron');
|
||||
|
||||
shell.openExternal(`https://duckduckgo.com?q=${encodeURIComponent(params.selectionText)}`);
|
||||
}
|
||||
},
|
||||
actions.inspect()
|
||||
]
|
||||
});
|
||||
|
||||
function onClosed() {
|
||||
// Dereference the window
|
||||
// For multiple windows store them in an array
|
||||
@@ -58,8 +79,8 @@ async function createMainWindow() {
|
||||
|
||||
mainWindowState.manage(win);
|
||||
|
||||
win.setMenu(null);
|
||||
win.loadURL('http://localhost:' + await port);
|
||||
win.setMenuBarVisibility(false);
|
||||
win.loadURL('http://127.0.0.1:' + await port);
|
||||
win.on('closed', onClosed);
|
||||
|
||||
win.webContents.on('new-window', (e, url) => {
|
||||
@@ -74,7 +95,7 @@ async function createMainWindow() {
|
||||
const parsedUrl = url.parse(targetUrl);
|
||||
|
||||
// we still need to allow internal redirects from setup and migration pages
|
||||
if (parsedUrl.hostname !== 'localhost' || (parsedUrl.path && parsedUrl.path !== '/')) {
|
||||
if (!['localhost', '127.0.0.1'].includes(parsedUrl.hostname) || (parsedUrl.path && parsedUrl.path !== '/')) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
Vendored
+4
-4
File diff suppressed because one or more lines are too long
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
@@ -3,7 +3,7 @@
|
||||
.CodeMirror {
|
||||
/* Set height, width, borders, and global font properties here */
|
||||
font-family: monospace;
|
||||
height: 300px;
|
||||
height: auto;
|
||||
color: black;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
@@ -3235,7 +3235,8 @@ var uniqueId = $.fn.extend( {
|
||||
node.makeVisible({ scrollIntoView: false });
|
||||
} catch (e) {} // #272
|
||||
if (activate === false) {
|
||||
return node.setFocus();
|
||||
node.setFocus();
|
||||
return _getResolvedPromise();
|
||||
}
|
||||
return node.setActive();
|
||||
}
|
||||
|
||||
Executable
+335
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
* printThis v1.15.0
|
||||
* @desc Printing plug-in for jQuery
|
||||
* @author Jason Day
|
||||
*
|
||||
* Resources (based on):
|
||||
* - jPrintArea: http://plugins.jquery.com/project/jPrintArea
|
||||
* - jqPrint: https://github.com/permanenttourist/jquery.jqprint
|
||||
* - Ben Nadal: http://www.bennadel.com/blog/1591-Ask-Ben-Print-Part-Of-A-Web-Page-With-jQuery.htm
|
||||
*
|
||||
* Licensed under the MIT licence:
|
||||
* http://www.opensource.org/licenses/mit-license.php
|
||||
*
|
||||
* (c) Jason Day 2015-2018
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* $("#mySelector").printThis({
|
||||
* debug: false, // show the iframe for debugging
|
||||
* importCSS: true, // import parent page css
|
||||
* importStyle: false, // import style tags
|
||||
* printContainer: true, // grab outer container as well as the contents of the selector
|
||||
* loadCSS: "path/to/my.css", // path to additional css file - use an array [] for multiple
|
||||
* pageTitle: "", // add title to print page
|
||||
* removeInline: false, // remove all inline styles from print elements
|
||||
* removeInlineSelector: "body *", // custom selectors to filter inline styles. removeInline must be true
|
||||
* printDelay: 333, // variable print delay
|
||||
* header: null, // prefix to html
|
||||
* footer: null, // postfix to html
|
||||
* base: false, // preserve the BASE tag, or accept a string for the URL
|
||||
* formValues: true, // preserve input/form values
|
||||
* canvas: false, // copy canvas elements
|
||||
* doctypeString: '...', // enter a different doctype for older markup
|
||||
* removeScripts: false, // remove script tags from print content
|
||||
* copyTagClasses: false // copy classes from the html & body tag
|
||||
* beforePrintEvent: null, // callback function for printEvent in iframe
|
||||
* beforePrint: null, // function called before iframe is filled
|
||||
* afterPrint: null // function called before iframe is removed
|
||||
* });
|
||||
*
|
||||
* Notes:
|
||||
* - the loadCSS will load additional CSS (with or without @media print) into the iframe, adjusting layout
|
||||
*/
|
||||
;
|
||||
(function($) {
|
||||
|
||||
function appendContent($el, content) {
|
||||
if (!content) return;
|
||||
|
||||
// Simple test for a jQuery element
|
||||
$el.append(content.jquery ? content.clone() : content);
|
||||
}
|
||||
|
||||
function appendBody($body, $element, opt) {
|
||||
// Clone for safety and convenience
|
||||
// Calls clone(withDataAndEvents = true) to copy form values.
|
||||
var $content = $element.clone(opt.formValues);
|
||||
|
||||
if (opt.formValues) {
|
||||
// Copy original select and textarea values to their cloned counterpart
|
||||
// Makes up for inability to clone select and textarea values with clone(true)
|
||||
copyValues($element, $content, 'select, textarea');
|
||||
}
|
||||
|
||||
if (opt.removeScripts) {
|
||||
$content.find('script').remove();
|
||||
}
|
||||
|
||||
if (opt.printContainer) {
|
||||
// grab $.selector as container
|
||||
$content.appendTo($body);
|
||||
} else {
|
||||
// otherwise just print interior elements of container
|
||||
$content.each(function() {
|
||||
$(this).children().appendTo($body)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Copies values from origin to clone for passed in elementSelector
|
||||
function copyValues(origin, clone, elementSelector) {
|
||||
var $originalElements = origin.find(elementSelector);
|
||||
|
||||
clone.find(elementSelector).each(function(index, item) {
|
||||
$(item).val($originalElements.eq(index).val());
|
||||
});
|
||||
}
|
||||
|
||||
var opt;
|
||||
$.fn.printThis = function(options) {
|
||||
opt = $.extend({}, $.fn.printThis.defaults, options);
|
||||
var $element = this instanceof jQuery ? this : $(this);
|
||||
|
||||
var strFrameName = "printThis-" + (new Date()).getTime();
|
||||
|
||||
if (window.location.hostname !== document.domain && navigator.userAgent.match(/msie/i)) {
|
||||
// Ugly IE hacks due to IE not inheriting document.domain from parent
|
||||
// checks if document.domain is set by comparing the host name against document.domain
|
||||
var iframeSrc = "javascript:document.write(\"<head><script>document.domain=\\\"" + document.domain + "\\\";</s" + "cript></head><body></body>\")";
|
||||
var printI = document.createElement('iframe');
|
||||
printI.name = "printIframe";
|
||||
printI.id = strFrameName;
|
||||
printI.className = "MSIE";
|
||||
document.body.appendChild(printI);
|
||||
printI.src = iframeSrc;
|
||||
|
||||
} else {
|
||||
// other browsers inherit document.domain, and IE works if document.domain is not explicitly set
|
||||
var $frame = $("<iframe id='" + strFrameName + "' name='printIframe' />");
|
||||
$frame.appendTo("body");
|
||||
}
|
||||
|
||||
var $iframe = $("#" + strFrameName);
|
||||
|
||||
// show frame if in debug mode
|
||||
if (!opt.debug) $iframe.css({
|
||||
position: "absolute",
|
||||
width: "0px",
|
||||
height: "0px",
|
||||
left: "-600px",
|
||||
top: "-600px"
|
||||
});
|
||||
|
||||
// before print callback
|
||||
if (typeof opt.beforePrint === "function") {
|
||||
opt.beforePrint();
|
||||
}
|
||||
|
||||
// $iframe.ready() and $iframe.load were inconsistent between browsers
|
||||
setTimeout(function() {
|
||||
|
||||
// Add doctype to fix the style difference between printing and render
|
||||
function setDocType($iframe, doctype){
|
||||
var win, doc;
|
||||
win = $iframe.get(0);
|
||||
win = win.contentWindow || win.contentDocument || win;
|
||||
doc = win.document || win.contentDocument || win;
|
||||
doc.open();
|
||||
doc.write(doctype);
|
||||
doc.close();
|
||||
}
|
||||
|
||||
if (opt.doctypeString){
|
||||
setDocType($iframe, opt.doctypeString);
|
||||
}
|
||||
|
||||
var $doc = $iframe.contents(),
|
||||
$head = $doc.find("head"),
|
||||
$body = $doc.find("body"),
|
||||
$base = $('base'),
|
||||
baseURL;
|
||||
|
||||
// add base tag to ensure elements use the parent domain
|
||||
if (opt.base === true && $base.length > 0) {
|
||||
// take the base tag from the original page
|
||||
baseURL = $base.attr('href');
|
||||
} else if (typeof opt.base === 'string') {
|
||||
// An exact base string is provided
|
||||
baseURL = opt.base;
|
||||
} else {
|
||||
// Use the page URL as the base
|
||||
baseURL = document.location.protocol + '//' + document.location.host;
|
||||
}
|
||||
|
||||
$head.append('<base href="' + baseURL + '">');
|
||||
|
||||
// import page stylesheets
|
||||
if (opt.importCSS) $("link[rel=stylesheet]").each(function() {
|
||||
var href = $(this).attr("href");
|
||||
if (href) {
|
||||
var media = $(this).attr("media") || "all";
|
||||
$head.append("<link type='text/css' rel='stylesheet' href='" + href + "' media='" + media + "'>");
|
||||
}
|
||||
});
|
||||
|
||||
// import style tags
|
||||
if (opt.importStyle) $("style").each(function() {
|
||||
$head.append(this.outerHTML);
|
||||
});
|
||||
|
||||
// add title of the page
|
||||
if (opt.pageTitle) $head.append("<title>" + opt.pageTitle + "</title>");
|
||||
|
||||
// import additional stylesheet(s)
|
||||
if (opt.loadCSS) {
|
||||
if ($.isArray(opt.loadCSS)) {
|
||||
jQuery.each(opt.loadCSS, function(index, value) {
|
||||
$head.append("<link type='text/css' rel='stylesheet' href='" + this + "'>");
|
||||
});
|
||||
} else {
|
||||
$head.append("<link type='text/css' rel='stylesheet' href='" + opt.loadCSS + "'>");
|
||||
}
|
||||
}
|
||||
|
||||
var pageHtml = $('html')[0];
|
||||
|
||||
// CSS VAR in html tag when dynamic apply e.g. document.documentElement.style.setProperty("--foo", bar);
|
||||
$doc.find('html').prop('style', pageHtml.style.cssText);
|
||||
|
||||
// copy 'root' tag classes
|
||||
var tag = opt.copyTagClasses;
|
||||
if (tag) {
|
||||
tag = tag === true ? 'bh' : tag;
|
||||
if (tag.indexOf('b') !== -1) {
|
||||
$body.addClass($('body')[0].className);
|
||||
}
|
||||
if (tag.indexOf('h') !== -1) {
|
||||
$doc.find('html').addClass(pageHtml.className);
|
||||
}
|
||||
}
|
||||
|
||||
// print header
|
||||
appendContent($body, opt.header);
|
||||
|
||||
if (opt.canvas) {
|
||||
// add canvas data-ids for easy access after cloning.
|
||||
var canvasId = 0;
|
||||
// .addBack('canvas') adds the top-level element if it is a canvas.
|
||||
$element.find('canvas').addBack('canvas').each(function(){
|
||||
$(this).attr('data-printthis', canvasId++);
|
||||
});
|
||||
}
|
||||
|
||||
appendBody($body, $element, opt);
|
||||
|
||||
if (opt.canvas) {
|
||||
// Re-draw new canvases by referencing the originals
|
||||
$body.find('canvas').each(function(){
|
||||
var cid = $(this).data('printthis'),
|
||||
$src = $('[data-printthis="' + cid + '"]');
|
||||
|
||||
this.getContext('2d').drawImage($src[0], 0, 0);
|
||||
|
||||
// Remove the markup from the original
|
||||
if ($.isFunction($.fn.removeAttr)) {
|
||||
$src.removeAttr('data-printthis');
|
||||
} else {
|
||||
$.each($src, function(i, el) {
|
||||
el.removeAttribute('data-printthis')
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// remove inline styles
|
||||
if (opt.removeInline) {
|
||||
// Ensure there is a selector, even if it's been mistakenly removed
|
||||
var selector = opt.removeInlineSelector || '*';
|
||||
// $.removeAttr available jQuery 1.7+
|
||||
if ($.isFunction($.removeAttr)) {
|
||||
$body.find(selector).removeAttr("style");
|
||||
} else {
|
||||
$body.find(selector).attr("style", "");
|
||||
}
|
||||
}
|
||||
|
||||
// print "footer"
|
||||
appendContent($body, opt.footer);
|
||||
|
||||
// attach event handler function to beforePrint event
|
||||
function attachOnBeforePrintEvent($iframe, beforePrintHandler) {
|
||||
var win = $iframe.get(0);
|
||||
win = win.contentWindow || win.contentDocument || win;
|
||||
|
||||
if (typeof beforePrintHandler === "function") {
|
||||
if ('matchMedia' in win) {
|
||||
win.matchMedia('print').addListener(function(mql) {
|
||||
if(mql.matches) beforePrintHandler();
|
||||
});
|
||||
} else {
|
||||
win.onbeforeprint = beforePrintHandler;
|
||||
}
|
||||
}
|
||||
}
|
||||
attachOnBeforePrintEvent($iframe, opt.beforePrint);
|
||||
|
||||
setTimeout(function() {
|
||||
if ($iframe.hasClass("MSIE")) {
|
||||
// check if the iframe was created with the ugly hack
|
||||
// and perform another ugly hack out of neccessity
|
||||
window.frames["printIframe"].focus();
|
||||
$head.append("<script> window.print(); </s" + "cript>");
|
||||
} else {
|
||||
// proper method
|
||||
if (document.queryCommandSupported("print")) {
|
||||
$iframe[0].contentWindow.document.execCommand("print", false, null);
|
||||
} else {
|
||||
$iframe[0].contentWindow.focus();
|
||||
$iframe[0].contentWindow.print();
|
||||
}
|
||||
}
|
||||
|
||||
// remove iframe after print
|
||||
if (!opt.debug) {
|
||||
setTimeout(function() {
|
||||
$iframe.remove();
|
||||
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// after print callback
|
||||
if (typeof opt.afterPrint === "function") {
|
||||
opt.afterPrint();
|
||||
}
|
||||
|
||||
}, opt.printDelay);
|
||||
|
||||
}, 333);
|
||||
|
||||
};
|
||||
|
||||
// defaults
|
||||
$.fn.printThis.defaults = {
|
||||
debug: false, // show the iframe for debugging
|
||||
importCSS: true, // import parent page css
|
||||
importStyle: false, // import style tags
|
||||
printContainer: true, // print outer container/$.selector
|
||||
loadCSS: "", // path to additional css file - use an array [] for multiple
|
||||
pageTitle: "", // add title to print page
|
||||
removeInline: false, // remove inline styles from print elements
|
||||
removeInlineSelector: "*", // custom selectors to filter inline styles. removeInline must be true
|
||||
printDelay: 333, // variable print delay
|
||||
header: null, // prefix to html
|
||||
footer: null, // postfix to html
|
||||
base: false, // preserve the BASE tag or accept a string for the URL
|
||||
formValues: true, // preserve input/form values
|
||||
canvas: false, // copy canvas content
|
||||
doctypeString: '<!DOCTYPE html>', // enter a different doctype for older markup
|
||||
removeScripts: false, // remove script tags from print content
|
||||
copyTagClasses: false, // copy classes from the html & body tag
|
||||
beforePrintEvent: null, // callback function for printEvent in iframe
|
||||
beforePrint: null, // function called before iframe is filled
|
||||
afterPrint: null // function called before iframe is removed
|
||||
};
|
||||
})(jQuery);
|
||||
Generated
+1169
-711
File diff suppressed because it is too large
Load Diff
+25
-23
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.33.0-beta",
|
||||
"version": "0.34.1",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
@@ -13,8 +13,8 @@
|
||||
"url": "https://github.com/zadam/trilium.git"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node ./src/www",
|
||||
"start-electron": "electron . --disable-gpu",
|
||||
"start-server": "TRILIUM_ENV=dev node ./src/www",
|
||||
"start-electron": "TRILIUM_ENV=dev electron . --disable-gpu",
|
||||
"build-backend-docs": "jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js",
|
||||
"build-frontend-docs": "jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/javascripts/entities/*.js src/public/javascripts/services/frontend_script_api.js",
|
||||
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs",
|
||||
@@ -28,23 +28,25 @@
|
||||
"commonmark": "0.29.0",
|
||||
"cookie-parser": "1.4.4",
|
||||
"csurf": "1.10.0",
|
||||
"dayjs": "1.8.14",
|
||||
"dayjs": "1.8.15",
|
||||
"debug": "4.1.1",
|
||||
"ejs": "2.6.1",
|
||||
"electron-debug": "3.0.0",
|
||||
"ejs": "2.6.2",
|
||||
"electron-context-menu": "0.13.0",
|
||||
"electron-debug": "3.0.1",
|
||||
"electron-dl": "1.14.0",
|
||||
"electron-find": "1.0.6",
|
||||
"electron-window-state": "5.0.3",
|
||||
"express": "4.17.1",
|
||||
"express-session": "1.16.2",
|
||||
"file-type": "11.1.0",
|
||||
"fs-extra": "8.0.1",
|
||||
"get-port": "5.0.0",
|
||||
"helmet": "3.18.0",
|
||||
"file-type": "12.0.1",
|
||||
"fs-extra": "8.1.0",
|
||||
"helmet": "3.19.0",
|
||||
"html": "1.0.0",
|
||||
"html2plaintext": "2.1.2",
|
||||
"http-proxy-agent": "^2.1.0",
|
||||
"https-proxy-agent": "^2.2.2",
|
||||
"image-type": "4.1.0",
|
||||
"imagemin": "6.1.0",
|
||||
"imagemin": "7.0.0",
|
||||
"imagemin-giflossy": "5.1.10",
|
||||
"imagemin-mozjpeg": "8.0.0",
|
||||
"imagemin-pngquant": "8.0.0",
|
||||
@@ -52,37 +54,37 @@
|
||||
"jimp": "0.6.4",
|
||||
"mime-types": "2.1.24",
|
||||
"moment": "2.24.0",
|
||||
"multer": "1.4.1",
|
||||
"node-abi": "2.8.0",
|
||||
"open": "6.3.0",
|
||||
"multer": "1.4.2",
|
||||
"node-abi": "2.9.0",
|
||||
"open": "6.4.0",
|
||||
"pngjs": "3.4.0",
|
||||
"portscanner": "2.2.0",
|
||||
"rand-token": "0.4.0",
|
||||
"rcedit": "2.0.0",
|
||||
"rimraf": "2.6.3",
|
||||
"sanitize-filename": "1.6.1",
|
||||
"sax": "1.2.4",
|
||||
"semver": "6.1.1",
|
||||
"semver": "6.2.0",
|
||||
"serve-favicon": "2.5.0",
|
||||
"session-file-store": "1.3.0",
|
||||
"simple-node-logger": "18.12.22",
|
||||
"session-file-store": "1.3.1",
|
||||
"simple-node-logger": "18.12.23",
|
||||
"sqlite": "3.0.3",
|
||||
"sqlite3": "4.0.8",
|
||||
"sqlite3": "4.0.9",
|
||||
"tar-stream": "2.1.0",
|
||||
"turndown": "5.0.3",
|
||||
"unescape": "1.0.1",
|
||||
"ws": "7.0.0",
|
||||
"ws": "7.1.0",
|
||||
"xml2js": "0.4.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"devtron": "1.4.0",
|
||||
"electron": "6.0.0-beta.7",
|
||||
"electron-builder": "20.43.0",
|
||||
"electron": "6.0.0-beta.14",
|
||||
"electron-builder": "21.1.1",
|
||||
"electron-compile": "6.4.4",
|
||||
"electron-installer-debian": "2.0.0",
|
||||
"electron-packager": "13.1.1",
|
||||
"electron-packager": "14.0.2",
|
||||
"electron-rebuild": "1.8.5",
|
||||
"lorem-ipsum": "2.0.3",
|
||||
"tape": "4.10.2",
|
||||
"xo": "0.24.0"
|
||||
},
|
||||
"xo": {
|
||||
|
||||
+3
-3
@@ -90,10 +90,10 @@ app.use((req, res, next) => {
|
||||
app.use((err, req, res, next) => {
|
||||
if (err && err.message && err.message.includes("Invalid package")) {
|
||||
// electron 6 outputs a lot of such errors which do not seem important
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(err);
|
||||
else {
|
||||
log.info(err);
|
||||
}
|
||||
|
||||
res.status(err.status || 500);
|
||||
res.send({
|
||||
|
||||
@@ -142,10 +142,30 @@ $noteTabContainer.on("click", ".export-note-button", function () {
|
||||
return;
|
||||
}
|
||||
|
||||
exportDialog.showDialog('single');
|
||||
exportDialog.showDialog(treeService.getActiveNode(), 'single');
|
||||
});
|
||||
|
||||
$noteTabContainer.on("click", ".import-files-button", importDialog.showDialog);
|
||||
$noteTabContainer.on("click", ".import-files-button", () => importDialog.showDialog(treeService.getActiveNode()));
|
||||
|
||||
$noteTabContainer.on("click", ".print-note-button", async function () {
|
||||
if ($(this).hasClass("disabled")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $tabContext = noteDetailService.getActiveTabContext();
|
||||
if (!$tabContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
await libraryLoader.requireLibrary(libraryLoader.PRINT_THIS);
|
||||
|
||||
$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",
|
||||
debug: true
|
||||
});
|
||||
});
|
||||
|
||||
$('[data-toggle="tooltip"]').tooltip({
|
||||
html: true
|
||||
|
||||
@@ -12,21 +12,19 @@ const $noteTitle = $('#branch-prefix-note-title');
|
||||
|
||||
let branchId;
|
||||
|
||||
async function showDialog() {
|
||||
async function showDialog(node) {
|
||||
utils.closeActiveDialog();
|
||||
|
||||
glob.activeDialog = $dialog;
|
||||
|
||||
$dialog.modal();
|
||||
|
||||
const currentNode = treeService.getActiveNode();
|
||||
|
||||
branchId = currentNode.data.branchId;
|
||||
branchId = node.data.branchId;
|
||||
const branch = await treeCache.getBranch(branchId);
|
||||
|
||||
$treePrefixInput.val(branch.prefix);
|
||||
|
||||
const noteTitle = await treeUtils.getNoteTitle(currentNode.data.noteId);
|
||||
const noteTitle = await treeUtils.getNoteTitle(node.data.noteId);
|
||||
|
||||
$noteTitle.text(" - " + noteTitle);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@ const $custom = $("#confirm-dialog-custom");
|
||||
const DELETE_NOTE_BUTTON_ID = "confirm-dialog-delete-note";
|
||||
|
||||
let resolve;
|
||||
let $originallyFocused; // element focused before the dialog was opened so we can return to it afterwards
|
||||
|
||||
function confirm(message) {
|
||||
$originallyFocused = $(':focus');
|
||||
|
||||
$custom.hide();
|
||||
|
||||
glob.activeDialog = $dialog;
|
||||
@@ -55,6 +58,11 @@ $dialog.on("hidden.bs.modal", () => {
|
||||
if (resolve) {
|
||||
resolve(false);
|
||||
}
|
||||
|
||||
if ($originallyFocused) {
|
||||
$originallyFocused.focus();
|
||||
$originallyFocused = null;
|
||||
}
|
||||
});
|
||||
|
||||
function doResolve(ret) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import treeService from '../services/tree.js';
|
||||
import treeUtils from "../services/tree_utils.js";
|
||||
import utils from "../services/utils.js";
|
||||
import messagingService from "../services/messaging.js";
|
||||
@@ -6,7 +5,7 @@ import infoService from "../services/info.js";
|
||||
|
||||
const $dialog = $("#export-dialog");
|
||||
const $form = $("#export-form");
|
||||
const $noteTitle = $dialog.find(".note-title");
|
||||
const $noteTitle = $dialog.find(".export-note-title");
|
||||
const $subtreeFormats = $("#export-subtree-formats");
|
||||
const $singleFormats = $("#export-single-formats");
|
||||
const $subtreeType = $("#export-type-subtree");
|
||||
@@ -17,8 +16,9 @@ const $exportButton = $("#export-button");
|
||||
const $opmlVersions = $("#opml-versions");
|
||||
|
||||
let exportId = '';
|
||||
let branchId = null;
|
||||
|
||||
async function showDialog(defaultType) {
|
||||
async function showDialog(node, defaultType) {
|
||||
utils.closeActiveDialog();
|
||||
|
||||
// each opening of the dialog resets the exportId so we don't associate it with previous exports anymore
|
||||
@@ -46,8 +46,9 @@ async function showDialog(defaultType) {
|
||||
|
||||
$dialog.modal();
|
||||
|
||||
const currentNode = treeService.getActiveNode();
|
||||
const noteTitle = await treeUtils.getNoteTitle(currentNode.data.noteId);
|
||||
branchId = node.data.branchId;
|
||||
|
||||
const noteTitle = await treeUtils.getNoteTitle(node.data.noteId);
|
||||
|
||||
$noteTitle.html(noteTitle);
|
||||
}
|
||||
@@ -70,9 +71,7 @@ $form.submit(() => {
|
||||
|
||||
const exportVersion = exportFormat === 'opml' ? $dialog.find("input[name='opml-version']:checked").val() : "1.0";
|
||||
|
||||
const currentNode = treeService.getActiveNode();
|
||||
|
||||
exportBranch(currentNode.data.branchId, exportType, exportFormat, exportVersion);
|
||||
exportBranch(branchId, exportType, exportFormat, exportVersion);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -19,8 +19,9 @@ const $codeImportedAsCodeCheckbox = $("#code-imported-as-code-checkbox");
|
||||
const $explodeArchivesCheckbox = $("#explode-archives-checkbox");
|
||||
|
||||
let importId;
|
||||
let importIntoNoteId = null;
|
||||
|
||||
async function showDialog() {
|
||||
async function showDialog(node) {
|
||||
utils.closeActiveDialog();
|
||||
|
||||
// each opening of the dialog resets the importId so we don't associate it with previous imports anymore
|
||||
@@ -37,19 +38,18 @@ async function showDialog() {
|
||||
|
||||
glob.activeDialog = $dialog;
|
||||
|
||||
const currentNode = treeService.getActiveNode();
|
||||
$noteTitle.text(await treeUtils.getNoteTitle(currentNode.data.noteId));
|
||||
importIntoNoteId = node.data.noteId;
|
||||
|
||||
$noteTitle.text(await treeUtils.getNoteTitle(importIntoNoteId));
|
||||
|
||||
$dialog.modal();
|
||||
}
|
||||
|
||||
$form.submit(() => {
|
||||
const currentNode = treeService.getActiveNode();
|
||||
|
||||
// disabling so that import is not triggered again.
|
||||
$importButton.attr("disabled", "disabled");
|
||||
|
||||
importIntoNote(currentNode.data.noteId);
|
||||
importIntoNote(importIntoNoteId);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -5,8 +5,11 @@ const $infoContent = $("#info-dialog-content");
|
||||
const $okButton = $("#info-dialog-ok-button");
|
||||
|
||||
let resolve;
|
||||
let $originallyFocused; // element focused before the dialog was opened so we can return to it afterwards
|
||||
|
||||
function info(message) {
|
||||
$originallyFocused = $(':focus');
|
||||
|
||||
utils.closeActiveDialog();
|
||||
|
||||
glob.activeDialog = $dialog;
|
||||
@@ -24,6 +27,11 @@ $dialog.on("hidden.bs.modal", () => {
|
||||
if (resolve) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
if ($originallyFocused) {
|
||||
$originallyFocused.focus();
|
||||
$originallyFocused = null;
|
||||
}
|
||||
});
|
||||
|
||||
$okButton.click(() => $dialog.modal("hide"));
|
||||
|
||||
@@ -3,8 +3,10 @@ import utils from './utils.js';
|
||||
import server from './server.js';
|
||||
import infoService from "./info.js";
|
||||
import treeCache from "./tree_cache.js";
|
||||
import treeUtils from "./tree_utils.js";
|
||||
import hoistedNoteService from "./hoisted_note.js";
|
||||
import noteDetailService from "./note_detail.js";
|
||||
import confirmDialog from "../dialogs/confirm.js";
|
||||
|
||||
async function moveBeforeNode(nodesToMove, beforeNode) {
|
||||
nodesToMove = await filterRootNote(nodesToMove);
|
||||
@@ -81,7 +83,7 @@ async function moveToNode(nodesToMove, toNode) {
|
||||
async function deleteNodes(nodes) {
|
||||
nodes = await filterRootNote(nodes);
|
||||
|
||||
if (nodes.length === 0 || !confirm('Are you sure you want to delete select note(s) and all the sub-notes?')) {
|
||||
if (nodes.length === 0 || !await confirmDialog.confirm('Are you sure you want to delete select note(s) and all the sub-notes?')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -101,13 +103,14 @@ async function deleteNodes(nodes) {
|
||||
next = nodes[0].getPrevSibling();
|
||||
}
|
||||
|
||||
if (!next && !hoistedNoteService.isTopLevelNode(nodes[0])) {
|
||||
if (!next && !await hoistedNoteService.isTopLevelNode(nodes[0])) {
|
||||
next = nodes[0].getParent();
|
||||
}
|
||||
|
||||
let activeNotePath = null;
|
||||
|
||||
if (next) {
|
||||
// activate next element after this one is deleted so we don't lose focus
|
||||
next.setActive();
|
||||
activeNotePath = await treeUtils.getNotePath(next);
|
||||
}
|
||||
|
||||
await treeService.loadTreeCache();
|
||||
@@ -119,7 +122,15 @@ async function deleteNodes(nodes) {
|
||||
}
|
||||
|
||||
for (const parentNoteId of parentNoteIds) {
|
||||
treeService.reloadNote(parentNoteId);
|
||||
await treeService.reloadNote(parentNoteId);
|
||||
}
|
||||
|
||||
// activate after all the reloading
|
||||
if (activeNotePath) {
|
||||
treeService.focusTree();
|
||||
|
||||
const node = await treeService.activateNote(activeNotePath);
|
||||
node.setFocus(true);
|
||||
}
|
||||
|
||||
infoService.showMessage("Note(s) has been deleted.");
|
||||
|
||||
@@ -39,7 +39,11 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
|
||||
* @param {string} notePath (or noteId)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
this.activateNote = treeService.activateNote;
|
||||
this.activateNote = async (notePath, noteLoadedListener) => {
|
||||
await treeService.activateNote(notePath, noteLoadedListener);
|
||||
|
||||
await treeService.scrollToActiveNote();
|
||||
};
|
||||
|
||||
/**
|
||||
* Activates newly created note. Compared to this.activateNote() also refreshes tree.
|
||||
|
||||
@@ -45,6 +45,8 @@ const LINK_MAP = {
|
||||
]
|
||||
};
|
||||
|
||||
const PRINT_THIS = {js: ["libraries/printThis.js"]};
|
||||
|
||||
async function requireLibrary(library) {
|
||||
if (library.css) {
|
||||
library.css.map(cssUrl => cssLoader.requireCss(cssUrl));
|
||||
@@ -79,5 +81,6 @@ export default {
|
||||
ESLINT,
|
||||
COMMONMARK,
|
||||
RELATION_MAP,
|
||||
LINK_MAP
|
||||
LINK_MAP,
|
||||
PRINT_THIS
|
||||
}
|
||||
@@ -67,12 +67,9 @@ function connectWebSocket() {
|
||||
|
||||
// use wss for secure messaging
|
||||
const ws = new WebSocket(protocol + "://" + location.host);
|
||||
ws.onopen = event => console.debug(utils.now(), "Connected to server with WebSocket");
|
||||
ws.onopen = () => console.debug(utils.now(), "Connected to server with WebSocket");
|
||||
ws.onmessage = handleMessage;
|
||||
ws.onclose = function(){
|
||||
// Try to reconnect in 5 seconds
|
||||
setTimeout(() => connectWebSocket(), 5000);
|
||||
};
|
||||
// we're not handling ws.onclose here because reconnection is done in sendPing()
|
||||
|
||||
return ws;
|
||||
}
|
||||
@@ -88,10 +85,17 @@ setTimeout(() => {
|
||||
console.log("Lost connection to server");
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'ping',
|
||||
lastSyncId: lastSyncId
|
||||
}));
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'ping',
|
||||
lastSyncId: lastSyncId
|
||||
}));
|
||||
}
|
||||
else if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
|
||||
console.log("WS closed or closing, trying to reconnect");
|
||||
|
||||
ws = connectWebSocket();
|
||||
}
|
||||
}, 1000);
|
||||
}, 0);
|
||||
|
||||
|
||||
@@ -76,6 +76,22 @@ function getActiveEditor() {
|
||||
}
|
||||
}
|
||||
|
||||
async function activateOrOpenNote(noteId) {
|
||||
for (const tabContext of tabContexts) {
|
||||
if (tabContext.note && tabContext.note.noteId === noteId) {
|
||||
await tabContext.activate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// if no tab with this note has been found we'll create new tab
|
||||
|
||||
await loadNoteDetail(noteId, {
|
||||
newTab: true,
|
||||
activate: true
|
||||
});
|
||||
}
|
||||
|
||||
function getTabContexts() {
|
||||
return tabContexts;
|
||||
}
|
||||
@@ -401,11 +417,14 @@ tabRow.addListener('activeTabChange', async ({ detail }) => {
|
||||
tabRow.addListener('tabRemove', async ({ detail }) => {
|
||||
const tabId = detail.tabEl.getAttribute('data-tab-id');
|
||||
|
||||
const tabContentToDelete = tabContexts.find(nc => nc.tabId === tabId);
|
||||
const tabContextToDelete = tabContexts.find(nc => nc.tabId === tabId);
|
||||
|
||||
if (tabContentToDelete) {
|
||||
await tabContentToDelete.saveNoteIfChanged();
|
||||
tabContentToDelete.$tabContent.remove();
|
||||
if (tabContextToDelete) {
|
||||
// sometimes there are orphan autocompletes after closing the tab
|
||||
tabContextToDelete.closeAutocomplete();
|
||||
|
||||
await tabContextToDelete.saveNoteIfChanged();
|
||||
tabContextToDelete.$tabContent.remove();
|
||||
}
|
||||
|
||||
tabContexts = tabContexts.filter(nc => nc.tabId !== tabId);
|
||||
@@ -541,6 +560,7 @@ export default {
|
||||
getTabContexts,
|
||||
getActiveTabContext,
|
||||
getActiveEditor,
|
||||
activateOrOpenNote,
|
||||
clearOpenTabsTask,
|
||||
filterTabs,
|
||||
openEmptyTab,
|
||||
|
||||
@@ -14,6 +14,7 @@ class NoteDetailCode {
|
||||
this.ctx = ctx;
|
||||
this.codeEditor = null;
|
||||
this.$component = ctx.$tabContent.find('.note-detail-code');
|
||||
this.$editorEl = this.$component.find('.note-detail-code-editor');
|
||||
this.$executeScriptButton = ctx.$tabContent.find(".execute-script-button");
|
||||
|
||||
utils.bindElShortcut(ctx.$tabContent, "ctrl+return", () => this.executeCurrentNote());
|
||||
@@ -34,7 +35,7 @@ class NoteDetailCode {
|
||||
|
||||
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
|
||||
|
||||
this.codeEditor = CodeMirror(this.$component[0], {
|
||||
this.codeEditor = CodeMirror(this.$editorEl[0], {
|
||||
value: "",
|
||||
viewportMargin: Infinity,
|
||||
indentUnit: 4,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import utils from "./utils.js";
|
||||
import noteDetailService from "./note_detail.js";
|
||||
import infoService from "./info.js";
|
||||
import server from "./server.js";
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ class NoteDetailText {
|
||||
constructor(ctx) {
|
||||
this.ctx = ctx;
|
||||
this.$component = ctx.$tabContent.find('.note-detail-text');
|
||||
this.$editorEl = this.$component.find('.note-detail-text-editor');
|
||||
this.textEditor = null;
|
||||
|
||||
this.$component.on("dblclick", "img", e => {
|
||||
@@ -39,7 +40,7 @@ class NoteDetailText {
|
||||
// textEditor might have been initialized during previous await so checking again
|
||||
// looks like double initialization can freeze CKEditor pretty badly
|
||||
if (!this.textEditor) {
|
||||
this.textEditor = await BalloonEditor.create(this.$component[0], {
|
||||
this.textEditor = await BalloonEditor.create(this.$editorEl[0], {
|
||||
placeholder: "Type the content of your note here ..."
|
||||
});
|
||||
|
||||
@@ -73,7 +74,7 @@ class NoteDetailText {
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.$component.focus();
|
||||
this.$editorEl.focus();
|
||||
}
|
||||
|
||||
getEditor() {
|
||||
|
||||
@@ -43,6 +43,7 @@ async function setupProtectedSession(password) {
|
||||
}
|
||||
|
||||
protectedSessionHolder.setProtectedSessionId(response.protectedSessionId);
|
||||
protectedSessionHolder.touchProtectedSession();
|
||||
|
||||
await treeService.reload();
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ function isProtectedSessionAvailable() {
|
||||
|
||||
function touchProtectedSession() {
|
||||
if (isProtectedSessionAvailable()) {
|
||||
lastProtectedSessionOperationDate = new Date();
|
||||
|
||||
setProtectedSessionId(utils.getCookie(PROTECTED_SESSION_ID_KEY));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,17 +67,27 @@ class TabContext {
|
||||
this.components = {};
|
||||
|
||||
this.$noteTitle.on('input', () => {
|
||||
if (!this.note) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.noteChanged();
|
||||
|
||||
const title = this.$noteTitle.val();
|
||||
this.note.title = this.$noteTitle.val();
|
||||
|
||||
this.tabRow.updateTab(this.$tab[0], {title});
|
||||
treeService.setNoteTitle(this.noteId, title);
|
||||
this.tabRow.updateTab(this.$tab[0], {title: this.note.title});
|
||||
treeService.setNoteTitle(this.noteId, this.note.title);
|
||||
|
||||
this.setTitleBar();
|
||||
});
|
||||
|
||||
if (utils.isDesktop()) {
|
||||
// keyboard plugin is not loaded in mobile
|
||||
this.$noteTitle.bind('keydown', 'return', () => this.getComponent().focus());
|
||||
this.$noteTitle.bind('keydown', 'return', () => {
|
||||
this.getComponent().focus();
|
||||
|
||||
return false; // to not propagate the enter into the editor (causes issues with codemirror)
|
||||
});
|
||||
}
|
||||
|
||||
this.$protectButton = this.$tabContent.find(".protect-button");
|
||||
@@ -86,7 +96,7 @@ class TabContext {
|
||||
this.$unprotectButton = this.$tabContent.find(".unprotect-button");
|
||||
this.$unprotectButton.click(protectedSessionService.unprotectNoteAndSendToServer);
|
||||
|
||||
console.log(`Created note tab ${this.tabId}`);
|
||||
console.debug(`Created note tab ${this.tabId}`);
|
||||
}
|
||||
|
||||
setNote(note, notePath) {
|
||||
@@ -103,6 +113,8 @@ class TabContext {
|
||||
|
||||
this.setTitleBar();
|
||||
|
||||
this.closeAutocomplete(); // esp. on windows autocomplete is not getting closed automatically
|
||||
|
||||
setTimeout(async () => {
|
||||
// we include the note into recent list only if the user stayed on the note at least 5 seconds
|
||||
if (notePath && notePath === this.notePath) {
|
||||
@@ -119,7 +131,7 @@ class TabContext {
|
||||
|
||||
this.showPaths();
|
||||
|
||||
console.log(`Switched tab ${this.tabId} to ${this.noteId}`);
|
||||
console.debug(`Switched tab ${this.tabId} to ${this.noteId}`);
|
||||
}
|
||||
|
||||
show() {
|
||||
@@ -328,6 +340,12 @@ class TabContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closeAutocomplete() {
|
||||
if (utils.isDesktop()) {
|
||||
this.$tabContent.find('.aa-input').autocomplete('close');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TabContext;
|
||||
@@ -196,6 +196,12 @@ async function resolveNotePath(notePath) {
|
||||
async function getRunPath(notePath) {
|
||||
utils.assertArguments(notePath);
|
||||
|
||||
notePath = notePath.split("-")[0].trim();
|
||||
|
||||
if (notePath.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = notePath.split("/").reverse();
|
||||
|
||||
if (!path.includes("root")) {
|
||||
@@ -303,6 +309,15 @@ function getSelectedNodes(stopOnParents = false) {
|
||||
return getTree().getSelectedNodes(stopOnParents);
|
||||
}
|
||||
|
||||
function getSelectedOrActiveNodes(node) {
|
||||
let notes = getSelectedNodes(true);
|
||||
|
||||
if (notes.length === 0) {
|
||||
notes.push(node);
|
||||
}
|
||||
return notes;
|
||||
}
|
||||
|
||||
function clearSelectedNodes() {
|
||||
for (const selectedNode of getSelectedNodes()) {
|
||||
selectedNode.setSelected(false);
|
||||
@@ -326,7 +341,32 @@ async function treeInitialized() {
|
||||
messagingService.logError("Cannot retrieve open tabs: " + e.stack);
|
||||
}
|
||||
|
||||
const filteredTabs = [];
|
||||
// if there's notePath in the URL, make sure it's open and active
|
||||
// (useful, among others, for opening clipped notes from clipper)
|
||||
if (location.hash) {
|
||||
const notePath = location.hash.substr(1);
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
if (await treeCache.noteExists(noteId)) {
|
||||
for (const tab of openTabs) {
|
||||
tab.active = false;
|
||||
}
|
||||
|
||||
const foundTab = openTabs.find(tab => noteId === treeUtils.getNoteIdFromNotePath(tab.notePath));
|
||||
|
||||
if (foundTab) {
|
||||
foundTab.active = true;
|
||||
}
|
||||
else {
|
||||
openTabs.push({
|
||||
notePath: notePath,
|
||||
active: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let filteredTabs = [];
|
||||
|
||||
for (const openTab of openTabs) {
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(openTab.notePath);
|
||||
@@ -337,6 +377,11 @@ async function treeInitialized() {
|
||||
}
|
||||
}
|
||||
|
||||
if (utils.isMobile()) {
|
||||
// mobile frontend doesn't have tabs so show only the active tab
|
||||
filteredTabs = filteredTabs.filter(tab => tab.active);
|
||||
}
|
||||
|
||||
if (filteredTabs.length === 0) {
|
||||
filteredTabs.push({
|
||||
notePath: 'root',
|
||||
@@ -453,12 +498,6 @@ function initFancyTree(tree) {
|
||||
$tree.on('contextmenu', '.fancytree-node', function(e) {
|
||||
const node = $.ui.fancytree.getNode(e);
|
||||
|
||||
// right click resets selection to just this node
|
||||
// this is important when e.g. you right click on a note while having different note active
|
||||
// and then click on delete - obviously you want to delete only that one right-clicked
|
||||
node.setSelected(true);
|
||||
clearSelectedNodes();
|
||||
|
||||
contextMenuWidget.initContextMenu(e, new TreeContextMenu(node));
|
||||
|
||||
return false; // blocks default browser right click menu
|
||||
@@ -522,17 +561,20 @@ async function collapseTree(node = null) {
|
||||
node.visit(node => node.setExpanded(false));
|
||||
}
|
||||
|
||||
function focusTree() {
|
||||
$tree.find('.fancytree-container').focus();
|
||||
}
|
||||
|
||||
async function scrollToActiveNote() {
|
||||
const activeContext = noteDetailService.getActiveTabContext();
|
||||
|
||||
if (activeContext && activeContext.notePath) {
|
||||
focusTree();
|
||||
|
||||
const node = await expandToNote(activeContext.notePath);
|
||||
|
||||
node.makeVisible({scrollIntoView: true});
|
||||
|
||||
node.setFocus();
|
||||
|
||||
await activateNote(activeContext.notePath);
|
||||
node.setFocus(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,7 +666,8 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
|
||||
extraClasses: await treeBuilder.getExtraClasses(noteEntity),
|
||||
icon: await treeBuilder.getIcon(noteEntity),
|
||||
folder: extraOptions.type === 'search',
|
||||
lazy: true
|
||||
lazy: true,
|
||||
key: utils.randomString(12) // this should prevent some "duplicate key" errors
|
||||
};
|
||||
|
||||
if (target === 'after') {
|
||||
@@ -632,10 +675,14 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
|
||||
}
|
||||
else if (target === 'into') {
|
||||
if (!node.getChildren() && node.isFolder()) {
|
||||
// folder is not loaded - load will bring up the note since it was already put into cache
|
||||
await node.load(true);
|
||||
|
||||
await node.setExpanded();
|
||||
}
|
||||
|
||||
node.addChildren(newNode);
|
||||
else {
|
||||
node.addChildren(newNode);
|
||||
}
|
||||
|
||||
await node.getLastChild().setActive(true);
|
||||
|
||||
@@ -701,6 +748,15 @@ messagingService.subscribeToMessages(message => {
|
||||
if (message.type === 'refresh-tree') {
|
||||
reload();
|
||||
}
|
||||
else if (message.type === 'open-note') {
|
||||
noteDetailService.activateOrOpenNote(message.noteId);
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const currentWindow = require("electron").remote.getCurrentWindow();
|
||||
|
||||
currentWindow.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
messagingService.subscribeToSyncMessages(syncData => {
|
||||
@@ -731,10 +787,12 @@ utils.bindShortcut('ctrl+o', async () => {
|
||||
async function createNoteInto() {
|
||||
const node = getActiveNode();
|
||||
|
||||
await createNote(node, node.data.noteId, 'into', {
|
||||
isProtected: node.data.isProtected,
|
||||
saveSelection: true
|
||||
});
|
||||
if (node) {
|
||||
await createNote(node, node.data.noteId, 'into', {
|
||||
isProtected: node.data.isProtected,
|
||||
saveSelection: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function checkFolderStatus(node) {
|
||||
@@ -808,6 +866,7 @@ export default {
|
||||
createNote,
|
||||
createNoteInto,
|
||||
getSelectedNodes,
|
||||
getSelectedOrActiveNodes,
|
||||
clearSelectedNodes,
|
||||
sortAlphabetically,
|
||||
showTree,
|
||||
@@ -821,5 +880,7 @@ export default {
|
||||
expandToNote,
|
||||
getNodeFromPath,
|
||||
resolveNotePath,
|
||||
getSomeNotePath
|
||||
getSomeNotePath,
|
||||
focusTree,
|
||||
scrollToActiveNote
|
||||
};
|
||||
@@ -83,7 +83,8 @@ async function prepareNode(branch) {
|
||||
icon: await getIcon(note),
|
||||
refKey: note.noteId,
|
||||
expanded: branch.isExpanded || hoistedNoteId === note.noteId,
|
||||
lazy: true
|
||||
lazy: true,
|
||||
key: utils.randomString(12) // this should prevent some "duplicate key" errors
|
||||
};
|
||||
|
||||
if (note.hasChildren() || note.type === 'search') {
|
||||
|
||||
@@ -34,45 +34,52 @@ class TreeContextMenu {
|
||||
const isNotRoot = note.noteId !== 'root';
|
||||
const isHoisted = note.noteId === await hoistedNoteService.getHoistedNoteId();
|
||||
|
||||
// some actions don't support multi-note so they are disabled when notes are selected
|
||||
// the only exception is when the only selected note is the one that was right-clicked, then
|
||||
// it's clear what the user meant to do.
|
||||
const selNodes = treeService.getSelectedNodes();
|
||||
const noSelectedNotes = selNodes.length === 0
|
||||
|| (selNodes.length === 1 && selNodes[0] === this.node);
|
||||
|
||||
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNote.type !== 'search';
|
||||
const insertChildNoteEnabled = note.type !== 'search';
|
||||
|
||||
return [
|
||||
{ title: "Open in new tab", cmd: "openInTab", uiIcon: "empty" },
|
||||
{ title: "Open in new tab", cmd: "openInTab", uiIcon: "empty", enabled: noSelectedNotes },
|
||||
{ title: "Insert note after <kbd>Ctrl+O</kbd>", cmd: "insertNoteAfter", uiIcon: "plus",
|
||||
items: insertNoteAfterEnabled ? this.getNoteTypeItems("insertNoteAfter") : null,
|
||||
enabled: insertNoteAfterEnabled },
|
||||
enabled: insertNoteAfterEnabled && noSelectedNotes },
|
||||
{ title: "Insert child note <kbd>Ctrl+P</kbd>", cmd: "insertChildNote", uiIcon: "plus",
|
||||
items: insertChildNoteEnabled ? this.getNoteTypeItems("insertChildNote") : null,
|
||||
enabled: insertChildNoteEnabled },
|
||||
enabled: insertChildNoteEnabled && noSelectedNotes },
|
||||
{ title: "Delete <kbd>Delete</kbd>", cmd: "delete", uiIcon: "trash",
|
||||
enabled: isNotRoot && !isHoisted && parentNote.type !== 'search' },
|
||||
{ title: "----" },
|
||||
isHoisted ? null : { title: "Hoist note <kbd>Ctrl-H</kbd>", cmd: "hoist", uiIcon: "empty" },
|
||||
isHoisted ? null : { title: "Hoist note <kbd>Ctrl-H</kbd>", cmd: "hoist", uiIcon: "empty", enabled: noSelectedNotes },
|
||||
!isHoisted || !isNotRoot ? null : { title: "Unhoist note <kbd>Ctrl-H</kbd>", cmd: "unhoist", uiIcon: "arrow-up" },
|
||||
{ title: "Edit branch prefix <kbd>F2</kbd>", cmd: "editBranchPrefix", uiIcon: "empty",
|
||||
enabled: isNotRoot && parentNote.type !== 'search'},
|
||||
enabled: isNotRoot && parentNote.type !== 'search' && noSelectedNotes},
|
||||
{ title: "----" },
|
||||
{ title: "Protect subtree", cmd: "protectSubtree", uiIcon: "shield-check" },
|
||||
{ title: "Unprotect subtree", cmd: "unprotectSubtree", uiIcon: "shield-close" },
|
||||
{ title: "Protect subtree", cmd: "protectSubtree", uiIcon: "shield-check", enabled: noSelectedNotes },
|
||||
{ title: "Unprotect subtree", cmd: "unprotectSubtree", uiIcon: "shield-close", enabled: noSelectedNotes },
|
||||
{ title: "----" },
|
||||
{ title: "Copy / clone <kbd>Ctrl+C</kbd>", cmd: "copy", uiIcon: "files",
|
||||
enabled: isNotRoot },
|
||||
{ title: "Cut <kbd>Ctrl+X</kbd>", cmd: "cut", uiIcon: "scissors",
|
||||
enabled: isNotRoot },
|
||||
{ title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "clipboard",
|
||||
enabled: !clipboard.isEmpty() && note.type !== 'search' },
|
||||
enabled: !clipboard.isEmpty() && note.type !== 'search' && noSelectedNotes },
|
||||
{ title: "Paste after", cmd: "pasteAfter", uiIcon: "clipboard",
|
||||
enabled: !clipboard.isEmpty() && isNotRoot && parentNote.type !== 'search' },
|
||||
enabled: !clipboard.isEmpty() && isNotRoot && parentNote.type !== 'search' && noSelectedNotes },
|
||||
{ title: "----" },
|
||||
{ title: "Export", cmd: "export", uiIcon: "empty",
|
||||
enabled: note.type !== 'search' },
|
||||
enabled: note.type !== 'search' && noSelectedNotes },
|
||||
{ title: "Import into note", cmd: "importIntoNote", uiIcon: "empty",
|
||||
enabled: note.type !== 'search' },
|
||||
enabled: note.type !== 'search' && noSelectedNotes },
|
||||
{ title: "----" },
|
||||
{ title: "Collapse subtree <kbd>Alt+-</kbd>", cmd: "collapseSubtree", uiIcon: "align-justify" },
|
||||
{ title: "Force note sync", cmd: "forceNoteSync", uiIcon: "refresh" },
|
||||
{ title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: "empty" }
|
||||
{ title: "Collapse subtree <kbd>Alt+-</kbd>", cmd: "collapseSubtree", uiIcon: "align-justify", enabled: noSelectedNotes },
|
||||
{ title: "Force note sync", cmd: "forceNoteSync", uiIcon: "refresh", enabled: noSelectedNotes },
|
||||
{ title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: "empty", enabled: noSelectedNotes }
|
||||
].filter(row => row !== null);
|
||||
}
|
||||
|
||||
@@ -110,10 +117,10 @@ class TreeContextMenu {
|
||||
protectedSessionService.protectSubtree(this.node.data.noteId, false);
|
||||
}
|
||||
else if (cmd === "copy") {
|
||||
clipboard.copy(treeService.getSelectedNodes());
|
||||
clipboard.copy(treeService.getSelectedOrActiveNodes(this.node));
|
||||
}
|
||||
else if (cmd === "cut") {
|
||||
clipboard.cut(treeService.getSelectedNodes());
|
||||
clipboard.cut(treeService.getSelectedOrActiveNodes(this.node));
|
||||
}
|
||||
else if (cmd === "pasteAfter") {
|
||||
clipboard.pasteAfter(this.node);
|
||||
@@ -122,13 +129,13 @@ class TreeContextMenu {
|
||||
clipboard.pasteInto(this.node);
|
||||
}
|
||||
else if (cmd === "delete") {
|
||||
treeChangesService.deleteNodes(treeService.getSelectedNodes(true));
|
||||
treeChangesService.deleteNodes(treeService.getSelectedOrActiveNodes(this.node));
|
||||
}
|
||||
else if (cmd === "export") {
|
||||
exportDialog.showDialog("subtree");
|
||||
exportDialog.showDialog(this.node,"subtree");
|
||||
}
|
||||
else if (cmd === "importIntoNote") {
|
||||
importDialog.showDialog();
|
||||
importDialog.showDialog(this.node);
|
||||
}
|
||||
else if (cmd === "collapseSubtree") {
|
||||
treeService.collapseTree(this.node);
|
||||
|
||||
@@ -7,7 +7,7 @@ import clipboard from "./clipboard.js";
|
||||
|
||||
const keyBindings = {
|
||||
"del": node => {
|
||||
treeChangesService.deleteNodes(treeService.getSelectedNodes(true));
|
||||
treeChangesService.deleteNodes(treeService.getSelectedOrActiveNodes(node));
|
||||
},
|
||||
"ctrl+up": node => {
|
||||
const beforeNode = node.getPrevSibling();
|
||||
@@ -104,13 +104,13 @@ const keyBindings = {
|
||||
|
||||
return false;
|
||||
},
|
||||
"ctrl+c": () => {
|
||||
clipboard.copy(treeService.getSelectedNodes());
|
||||
"ctrl+c": node => {
|
||||
clipboard.copy(treeService.getSelectedOrActiveNodes(node));
|
||||
|
||||
return false;
|
||||
},
|
||||
"ctrl+x": () => {
|
||||
clipboard.cut(treeService.getSelectedNodes());
|
||||
"ctrl+x": node => {
|
||||
clipboard.cut(treeService.getSelectedOrActiveNodes(node));
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
@@ -19,7 +19,10 @@ function getNoteIdFromNotePath(notePath) {
|
||||
|
||||
const path = notePath.split("/");
|
||||
|
||||
return path[path.length - 1];
|
||||
const lastSegment = path[path.length - 1];
|
||||
|
||||
// path could have also tabId suffix
|
||||
return lastSegment.split("-")[0];
|
||||
}
|
||||
|
||||
async function getNotePath(node) {
|
||||
@@ -38,7 +41,9 @@ async function getNotePath(node) {
|
||||
node = node.getParent();
|
||||
}
|
||||
|
||||
path.push(node.data.noteId); // root or hoisted noteId
|
||||
if (node) { // null node can happen directly after unhoisting when tree is still hoisted but option has been changed already
|
||||
path.push(node.data.noteId); // root or hoisted noteId
|
||||
}
|
||||
|
||||
return path.reverse().join("/");
|
||||
}
|
||||
|
||||
@@ -20,8 +20,7 @@ body {
|
||||
|
||||
#container.distraction-free-mode {
|
||||
grid-template-areas:
|
||||
"title"
|
||||
"note-detail" !important;
|
||||
"tab-container" !important;
|
||||
grid-template-rows: auto
|
||||
auto
|
||||
!important;
|
||||
|
||||
@@ -135,13 +135,16 @@ ul.fancytree-container {
|
||||
.note-detail-text h6 { font-size: 1.1em; }
|
||||
|
||||
.note-detail-text {
|
||||
overflow: auto;
|
||||
font-family: var(--detail-text-font-family);
|
||||
}
|
||||
|
||||
.note-detail-text-editor {
|
||||
padding-top: 10px;
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
/* This is because with empty content height of editor is 0 and it's impossible to click into it */
|
||||
min-height: 200px;
|
||||
padding-top: 10px;
|
||||
overflow: auto;
|
||||
font-family: var(--detail-text-font-family);
|
||||
}
|
||||
|
||||
.note-detail-text p:first-child, .note-detail-text::before {
|
||||
@@ -190,27 +193,32 @@ span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-tit
|
||||
|
||||
span.fancytree-active .fancytree-title {
|
||||
font-weight: bold;
|
||||
border-color: var(--main-background-color) !important; /* invisible border */
|
||||
border-color: var(--main-border-color) !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-active:not(.fancytree-focused) .fancytree-title {
|
||||
border-style: dashed !important;
|
||||
}
|
||||
|
||||
span.fancytree-focused .fancytree-title, span.fancytree-focused.fancytree-selected .fancytree-title {
|
||||
color: var(--active-item-text-color) !important;
|
||||
background-color: var(--active-item-background-color) !important;
|
||||
border-color: var(--main-background-color) !important; /* invisible border */
|
||||
border-radius: 3px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-selected .fancytree-title {
|
||||
color: var(--hover-item-text-color) !important;
|
||||
background-color: var(--hover-item-background-color) !important;
|
||||
border-color: var(--main-background-color) !important; /* invisible border */
|
||||
border-radius: 3px;
|
||||
border-radius: 5px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
span.fancytree-node:hover span.fancytree-title {
|
||||
border-color: var(--main-border-color) !important;
|
||||
border-radius: 3px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.ui-autocomplete {
|
||||
@@ -349,10 +357,13 @@ div.ui-tooltip {
|
||||
}
|
||||
|
||||
.note-detail-code {
|
||||
min-height: 200px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.note-detail-code-editor {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.note-detail-render {
|
||||
min-height: 200px;
|
||||
}
|
||||
@@ -827,4 +838,8 @@ a.external:after, a[href^="http://"]:after, a[href^="https://"]:after {
|
||||
|
||||
.note-detail-empty {
|
||||
margin: 50px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 0.7rem 1rem !important; /* make modal header padding slightly smaller */
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
"use strict";
|
||||
|
||||
const noteService = require('../../services/notes');
|
||||
const dateNoteService = require('../../services/date_notes');
|
||||
const dateUtils = require('../../services/date_utils');
|
||||
const imageService = require('../../services/image');
|
||||
const appInfo = require('../../services/app_info');
|
||||
const messagingService = require('../../services/messaging');
|
||||
const log = require('../../services/log');
|
||||
const utils = require('../../services/utils');
|
||||
const path = require('path');
|
||||
const Link = require('../../entities/link');
|
||||
|
||||
async function findClippingNote(todayNote, pageUrl) {
|
||||
const notes = await todayNote.getDescendantNotesWithLabel('pageUrl', pageUrl);
|
||||
|
||||
for (const note of notes) {
|
||||
if (await note.getLabelValue('clipType') === 'clippings') {
|
||||
return note;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function addClipping(req) {
|
||||
const {title, content, pageUrl, images} = req.body;
|
||||
|
||||
const todayNote = await dateNoteService.getDateNote(dateUtils.localNowDate());
|
||||
|
||||
let clippingNote = await findClippingNote(todayNote, pageUrl);
|
||||
|
||||
if (!clippingNote) {
|
||||
clippingNote = (await noteService.createNote(todayNote.noteId, title, '')).note;
|
||||
|
||||
await clippingNote.setLabel('clipType', 'clippings');
|
||||
await clippingNote.setLabel('pageUrl', pageUrl);
|
||||
}
|
||||
|
||||
const rewrittenContent = await addImagesToNote(images, clippingNote, content);
|
||||
|
||||
await clippingNote.setContent(await clippingNote.getContent() + '<p>' + rewrittenContent + '</p>');
|
||||
|
||||
return {
|
||||
noteId: clippingNote.noteId
|
||||
};
|
||||
}
|
||||
|
||||
async function createNote(req) {
|
||||
const {title, content, pageUrl, images, clipType} = req.body;
|
||||
|
||||
const todayNote = await dateNoteService.getDateNote(dateUtils.localNowDate());
|
||||
|
||||
const {note} = await noteService.createNote(todayNote.noteId, title, content);
|
||||
|
||||
await note.setLabel('clipType', clipType);
|
||||
|
||||
if (pageUrl) {
|
||||
await note.setLabel('pageUrl', pageUrl);
|
||||
}
|
||||
|
||||
const rewrittenContent = await addImagesToNote(images, note, content);
|
||||
|
||||
await note.setContent(rewrittenContent);
|
||||
|
||||
return {
|
||||
noteId: note.noteId
|
||||
};
|
||||
}
|
||||
|
||||
async function addImagesToNote(images, note, content) {
|
||||
let rewrittenContent = content;
|
||||
|
||||
if (images) {
|
||||
for (const {src, dataUrl, imageId} of images) {
|
||||
const filename = path.basename(src);
|
||||
|
||||
if (!dataUrl.startsWith("data:image")) {
|
||||
log.info("Image could not be recognized as data URL:", dataUrl.substr(0, Math.min(100, dataUrl.length)));
|
||||
continue;
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(dataUrl.split(",")[1], 'base64');
|
||||
|
||||
const {note: imageNote, url} = await imageService.saveImage(buffer, filename, note.noteId, true);
|
||||
|
||||
await new Link({
|
||||
noteId: note.noteId,
|
||||
targetNoteId: imageNote.noteId,
|
||||
type: 'image'
|
||||
}).save();
|
||||
|
||||
console.log(`Replacing ${imageId} with ${url}`);
|
||||
|
||||
rewrittenContent = rewrittenContent.replace(imageId, url);
|
||||
}
|
||||
}
|
||||
|
||||
return rewrittenContent;
|
||||
}
|
||||
|
||||
async function openNote(req) {
|
||||
if (utils.isElectron()) {
|
||||
messagingService.sendMessageToAllClients({
|
||||
type: 'open-note',
|
||||
noteId: req.params.noteId
|
||||
});
|
||||
|
||||
return {
|
||||
result: 'ok'
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
result: 'open-in-browser'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handshake() {
|
||||
return {
|
||||
appName: "trilium",
|
||||
protocolVersion: appInfo.clipperProtocolVersion
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createNote,
|
||||
addClipping,
|
||||
openNote,
|
||||
handshake
|
||||
};
|
||||
@@ -34,7 +34,7 @@ async function uploadImage(req) {
|
||||
return [404, `Note ${noteId} doesn't exist.`];
|
||||
}
|
||||
|
||||
if (!["image/png", "image/jpeg", "image/gif"].includes(file.mimetype)) {
|
||||
if (!["image/png", "image/jpeg", "image/gif", "image/webp"].includes(file.mimetype)) {
|
||||
return [400, "Unknown image type: " + file.mimetype];
|
||||
}
|
||||
|
||||
|
||||
+24
-1
@@ -11,6 +11,8 @@ const eventService = require('../../services/events');
|
||||
const cls = require('../../services/cls');
|
||||
const sqlInit = require('../../services/sql_init');
|
||||
const sql = require('../../services/sql');
|
||||
const optionService = require('../../services/options');
|
||||
const ApiToken = require('../../entities/api_token');
|
||||
|
||||
async function loginSync(req) {
|
||||
if (!await sqlInit.schemaExists()) {
|
||||
@@ -76,7 +78,28 @@ async function loginToProtectedSession(req) {
|
||||
};
|
||||
}
|
||||
|
||||
async function token(req) {
|
||||
const username = req.body.username;
|
||||
const password = req.body.password;
|
||||
|
||||
const isUsernameValid = username === await optionService.getOption('username');
|
||||
const isPasswordValid = await passwordEncryptionService.verifyPassword(password);
|
||||
|
||||
if (!isUsernameValid || !isPasswordValid) {
|
||||
return [401, "Incorrect username/password"];
|
||||
}
|
||||
|
||||
const apiToken = await new ApiToken({
|
||||
token: utils.randomSecureToken()
|
||||
}).save();
|
||||
|
||||
return {
|
||||
token: apiToken.token
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loginSync,
|
||||
loginToProtectedSession
|
||||
loginToProtectedSession,
|
||||
token
|
||||
};
|
||||
@@ -1,33 +1,9 @@
|
||||
"use strict";
|
||||
|
||||
const imageType = require('image-type');
|
||||
const imageService = require('../../services/image');
|
||||
const utils = require('../../services/utils');
|
||||
const dateNoteService = require('../../services/date_notes');
|
||||
const sql = require('../../services/sql');
|
||||
const noteService = require('../../services/notes');
|
||||
const passwordEncryptionService = require('../../services/password_encryption');
|
||||
const optionService = require('../../services/options');
|
||||
const ApiToken = require('../../entities/api_token');
|
||||
|
||||
async function login(req) {
|
||||
const username = req.body.username;
|
||||
const password = req.body.password;
|
||||
|
||||
const isUsernameValid = username === await optionService.getOption('username');
|
||||
const isPasswordValid = await passwordEncryptionService.verifyPassword(password);
|
||||
|
||||
if (!isUsernameValid || !isPasswordValid) {
|
||||
return [401, "Incorrect username/password"];
|
||||
}
|
||||
|
||||
const apiToken = await new ApiToken({
|
||||
token: utils.randomSecureToken()
|
||||
}).save();
|
||||
|
||||
return {
|
||||
token: apiToken.token
|
||||
};
|
||||
}
|
||||
|
||||
async function uploadImage(req) {
|
||||
const file = req.file;
|
||||
@@ -36,9 +12,11 @@ async function uploadImage(req) {
|
||||
return [400, "Unknown image type: " + file.mimetype];
|
||||
}
|
||||
|
||||
const originalName = "Sender image." + imageType(file.buffer).ext;
|
||||
|
||||
const parentNote = await dateNoteService.getDateNote(req.headers['x-local-date']);
|
||||
|
||||
const {noteId} = await imageService.saveImage(file.buffer, "Sender image", parentNote.noteId, true);
|
||||
const {noteId} = await imageService.saveImage(file.buffer, originalName, parentNote.noteId, true);
|
||||
|
||||
return {
|
||||
noteId: noteId
|
||||
@@ -64,7 +42,6 @@ async function saveNote(req) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
login,
|
||||
uploadImage,
|
||||
saveNote
|
||||
};
|
||||
+14
-3
@@ -1,6 +1,7 @@
|
||||
const setupRoute = require('./setup');
|
||||
const loginRoute = require('./login');
|
||||
const indexRoute = require('./index');
|
||||
const utils = require('../services/utils');
|
||||
const multer = require('multer')();
|
||||
|
||||
// API routes
|
||||
@@ -32,6 +33,7 @@ const filesRoute = require('./api/file_upload');
|
||||
const searchRoute = require('./api/search');
|
||||
const dateNotesRoute = require('./api/date_notes');
|
||||
const linkMapRoute = require('./api/link_map');
|
||||
const clipperRoute = require('./api/clipper');
|
||||
|
||||
const log = require('../services/log');
|
||||
const express = require('express');
|
||||
@@ -212,9 +214,9 @@ function register(app) {
|
||||
apiRoute(GET, '/api/script/relation/:noteId/:relationName', scriptRoute.getRelationBundles);
|
||||
|
||||
// no CSRF since this is called from android app
|
||||
route(POST, '/api/sender/login', [], senderRoute.login, apiResultHandler);
|
||||
route(POST, '/api/sender/image', [auth.checkSenderToken, uploadMiddleware], senderRoute.uploadImage, apiResultHandler);
|
||||
route(POST, '/api/sender/note', [auth.checkSenderToken], senderRoute.saveNote, apiResultHandler);
|
||||
route(POST, '/api/sender/login', [], loginApiRoute.token, apiResultHandler);
|
||||
route(POST, '/api/sender/image', [auth.checkToken, uploadMiddleware], senderRoute.uploadImage, apiResultHandler);
|
||||
route(POST, '/api/sender/note', [auth.checkToken], senderRoute.saveNote, apiResultHandler);
|
||||
|
||||
apiRoute(GET, '/api/search/:searchString', searchRoute.searchNotes);
|
||||
apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote);
|
||||
@@ -222,6 +224,15 @@ function register(app) {
|
||||
route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler);
|
||||
// this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
|
||||
apiRoute(POST, '/api/login/protected', loginApiRoute.loginToProtectedSession);
|
||||
route(POST, '/api/login/token', [], loginApiRoute.token, apiResultHandler);
|
||||
|
||||
// in case of local electron, local calls are allowed unauthenticated, for server they need auth
|
||||
const clipperMiddleware = utils.isElectron() ? [] : [auth.checkToken];
|
||||
|
||||
route(GET, '/api/clipper/handshake', clipperMiddleware, clipperRoute.handshake, apiResultHandler);
|
||||
route(POST, '/api/clipper/clippings', clipperMiddleware, clipperRoute.addClipping, apiResultHandler);
|
||||
route(POST, '/api/clipper/notes', clipperMiddleware, clipperRoute.createNote, apiResultHandler);
|
||||
route(POST, '/api/clipper/open/:noteId', clipperMiddleware, clipperRoute.openNote, apiResultHandler);
|
||||
|
||||
app.use('', router);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const {TRILIUM_DATA_DIR} = require('./data_dir');
|
||||
|
||||
const APP_DB_VERSION = 136;
|
||||
const SYNC_VERSION = 9;
|
||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||
|
||||
module.exports = {
|
||||
appVersion: packageJson.version,
|
||||
@@ -13,5 +14,6 @@ module.exports = {
|
||||
syncVersion: SYNC_VERSION,
|
||||
buildDate: build.buildDate,
|
||||
buildRevision: build.buildRevision,
|
||||
dataDirectory: TRILIUM_DATA_DIR
|
||||
dataDirectory: TRILIUM_DATA_DIR,
|
||||
clipperProtocolVersion: CLIPPER_PROTOCOL_VERSION
|
||||
};
|
||||
@@ -56,7 +56,7 @@ async function checkAppNotInitialized(req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
async function checkSenderToken(req, res, next) {
|
||||
async function checkToken(req, res, next) {
|
||||
const token = req.headers.authorization;
|
||||
|
||||
if (await sql.getValue("SELECT COUNT(*) FROM api_tokens WHERE isDeleted = 0 AND token = ?", [token]) === 0) {
|
||||
@@ -89,6 +89,6 @@ module.exports = {
|
||||
checkAppInitialized,
|
||||
checkAppNotInitialized,
|
||||
checkApiAuthOrElectron,
|
||||
checkSenderToken,
|
||||
checkToken,
|
||||
checkBasicAuth
|
||||
};
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2019-06-12T21:45:05+02:00", buildRevision: "c646019536dbc48f5f3ea1a35e4df844a4a2c8c3" };
|
||||
module.exports = { buildDate:"2019-07-28T15:02:36+02:00", buildRevision: "083dee9bdd98ff4579fbd221e3b3d5f9a871b685" };
|
||||
|
||||
@@ -29,6 +29,7 @@ async function getNoteStartingWith(parentNoteId, startsWith) {
|
||||
AND branches.isDeleted = 0`, [parentNoteId]);
|
||||
}
|
||||
|
||||
/** @return {Promise<Note>} */
|
||||
async function getRootCalendarNote() {
|
||||
// some caching here could be useful (e.g. in CLS)
|
||||
let rootNote = await attributeService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
|
||||
@@ -47,6 +48,7 @@ async function getRootCalendarNote() {
|
||||
return rootNote;
|
||||
}
|
||||
|
||||
/** @return {Promise<Note>} */
|
||||
async function getYearNote(dateStr, rootNote) {
|
||||
if (!rootNote) {
|
||||
rootNote = await getRootCalendarNote();
|
||||
@@ -79,6 +81,7 @@ async function getMonthNoteTitle(rootNote, monthNumber, dateObj) {
|
||||
.replace(/{month}/g, monthName);
|
||||
}
|
||||
|
||||
/** @return {Promise<Note>} */
|
||||
async function getMonthNote(dateStr, rootNote) {
|
||||
const monthStr = dateStr.substr(0, 7);
|
||||
const monthNumber = dateStr.substr(5, 2);
|
||||
@@ -116,6 +119,7 @@ async function getDateNoteTitle(rootNote, dayNumber, dateObj) {
|
||||
.replace(/{weekDay2}/g, weekDay.substr(0, 2));
|
||||
}
|
||||
|
||||
/** @return {Promise<Note>} */
|
||||
async function getDateNote(dateStr) {
|
||||
const rootNote = await getRootCalendarNote();
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
isDev: function () {
|
||||
return process.env.TRILIUM_ENV && process.env.TRILIUM_ENV === 'dev';
|
||||
}
|
||||
};
|
||||
@@ -32,11 +32,11 @@ async function exportToTar(exportContext, branch, format, res) {
|
||||
do {
|
||||
index = existingFileNames[lcFileName]++;
|
||||
|
||||
newName = lcFileName + "_" + index;
|
||||
newName = index + "_" + lcFileName;
|
||||
}
|
||||
while (newName in existingFileNames);
|
||||
|
||||
return fileName + "_" + index;
|
||||
return index + "_" + fileName;
|
||||
}
|
||||
else {
|
||||
existingFileNames[lcFileName] = 1;
|
||||
@@ -46,24 +46,32 @@ async function exportToTar(exportContext, branch, format, res) {
|
||||
}
|
||||
|
||||
function getDataFileName(note, baseFileName, existingFileNames) {
|
||||
let extension;
|
||||
const existingExtension = path.extname(baseFileName).toLowerCase();
|
||||
let newExtension;
|
||||
|
||||
// following two are handled specifically since we always want to have these extensions no matter the automatic detection
|
||||
// and/or existing detected extensions in the note name
|
||||
if (note.type === 'text' && format === 'markdown') {
|
||||
extension = 'md';
|
||||
newExtension = 'md';
|
||||
}
|
||||
else if (note.type === 'text' && format === 'html') {
|
||||
newExtension = 'html';
|
||||
}
|
||||
else if (note.mime === 'application/x-javascript' || note.mime === 'text/javascript') {
|
||||
extension = 'js';
|
||||
newExtension = 'js';
|
||||
}
|
||||
else if (existingExtension.length > 0) { // if the page already has an extension, then we'll just keep it
|
||||
newExtension = null;
|
||||
}
|
||||
else {
|
||||
extension = mimeTypes.extension(note.mime) || "dat";
|
||||
newExtension = mimeTypes.extension(note.mime) || "dat";
|
||||
}
|
||||
|
||||
let fileName = baseFileName;
|
||||
const existingExtension = path.extname(fileName).toLowerCase();
|
||||
|
||||
// if the note is already named with extension (e.g. "jquery.js"), then it's silly to append exact same extension again
|
||||
if (existingExtension !== extension) {
|
||||
fileName += "." + extension;
|
||||
if (newExtension && existingExtension !== "." + newExtension.toLowerCase()) {
|
||||
fileName += "." + newExtension;
|
||||
}
|
||||
|
||||
return getUniqueFilename(existingFileNames, fileName);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
const config = require('./config');
|
||||
const env = require('./env');
|
||||
|
||||
let environmentHost;
|
||||
|
||||
if (process.env.TRILIUM_HOST) {
|
||||
environmentHost = process.env.TRILIUM_HOST;
|
||||
}
|
||||
|
||||
module.exports = Promise.resolve(environmentHost || config['Network']['host'] || '0.0.0.0');
|
||||
+10
-4
@@ -13,14 +13,20 @@ const imageType = require('image-type');
|
||||
const sanitizeFilename = require('sanitize-filename');
|
||||
|
||||
async function saveImage(buffer, originalName, parentNoteId, shrinkImageSwitch) {
|
||||
const origImageFormat = imageType(buffer);
|
||||
|
||||
if (origImageFormat.ext === "webp") {
|
||||
// JIMP does not support webp at the moment: https://github.com/oliver-moran/jimp/issues/144
|
||||
shrinkImageSwitch = false;
|
||||
}
|
||||
|
||||
const finalImageBuffer = shrinkImageSwitch ? await shrinkImage(buffer, originalName) : buffer;
|
||||
|
||||
const imageFormat = imageType(finalImageBuffer);
|
||||
|
||||
const parentNote = await repository.getNote(parentNoteId);
|
||||
|
||||
const fileNameWithoutExtension = originalName.replace(/\.[^/.]+$/, "");
|
||||
const fileName = sanitizeFilename(fileNameWithoutExtension + "." + imageFormat.ext);
|
||||
const fileName = sanitizeFilename(originalName);
|
||||
|
||||
const {note} = await noteService.createNote(parentNoteId, fileName, finalImageBuffer, {
|
||||
target: 'into',
|
||||
@@ -48,7 +54,7 @@ async function shrinkImage(buffer, originalName) {
|
||||
try {
|
||||
finalImageBuffer = await optimize(resizedImage);
|
||||
} catch (e) {
|
||||
log.error("Failed to optimize image '" + originalName + "\nStack: " + e.stack);
|
||||
log.error("Failed to optimize image '" + originalName + "'\nStack: " + e.stack);
|
||||
finalImageBuffer = resizedImage;
|
||||
}
|
||||
|
||||
@@ -93,7 +99,7 @@ async function optimize(buffer) {
|
||||
quality: 50
|
||||
}),
|
||||
imageminPngQuant({
|
||||
quality: "0-70"
|
||||
quality: [0, 0.7]
|
||||
}),
|
||||
imageminGifLossy({
|
||||
lossy: 80,
|
||||
|
||||
@@ -251,7 +251,7 @@ async function importEnex(importContext, file, parentNote) {
|
||||
noteContent = noteContent.replace(mediaRegex, resourceLink);
|
||||
};
|
||||
|
||||
if (["image/jpeg", "image/png", "image/gif"].includes(resource.mime)) {
|
||||
if (["image/jpeg", "image/png", "image/gif", "image/webp"].includes(resource.mime)) {
|
||||
try {
|
||||
const originalName = "image." + resource.mime.substr(6);
|
||||
|
||||
|
||||
@@ -74,11 +74,7 @@ function getMime(fileName) {
|
||||
|
||||
const ext = path.extname(fileName).toLowerCase();
|
||||
|
||||
console.log("EXT", ext);
|
||||
|
||||
if (ext in EXTENSION_TO_MIME) {
|
||||
console.log(EXTENSION_TO_MIME[ext]);
|
||||
|
||||
return EXTENSION_TO_MIME[ext];
|
||||
}
|
||||
|
||||
@@ -102,7 +98,7 @@ async function importSingleFile(importContext, file, parentNote) {
|
||||
return await importCodeNote(importContext, file, parentNote);
|
||||
}
|
||||
|
||||
if (["image/jpeg", "image/gif", "image/png"].includes(mime)) {
|
||||
if (["image/jpeg", "image/gif", "image/png", "image/webp"].includes(mime)) {
|
||||
return await importImage(file, parentNote, importContext);
|
||||
}
|
||||
|
||||
@@ -110,7 +106,7 @@ async function importSingleFile(importContext, file, parentNote) {
|
||||
}
|
||||
|
||||
async function importImage(file, parentNote, importContext) {
|
||||
const {note} = await imageService.saveImage(file.buffer, getFileNameWithoutExtension(file.originalname), parentNote.noteId, importContext.shrinkImages);
|
||||
const {note} = await imageService.saveImage(file.buffer, file.originalname, parentNote.noteId, importContext.shrinkImages);
|
||||
|
||||
importContext.increaseProgressCount();
|
||||
|
||||
|
||||
+21
-3
@@ -1,10 +1,28 @@
|
||||
const getPort = require('get-port');
|
||||
const config = require('./config');
|
||||
const utils = require('./utils');
|
||||
const env = require('./env');
|
||||
const portscanner = require('portscanner');
|
||||
|
||||
let environmentPort;
|
||||
|
||||
if (process.env.TRILIUM_PORT) {
|
||||
environmentPort = parseInt(process.env.TRILIUM_PORT);
|
||||
}
|
||||
|
||||
if (utils.isElectron()) {
|
||||
module.exports = getPort();
|
||||
module.exports = new Promise((resolve, reject) => {
|
||||
const startingPort = environmentPort || (env.isDev() ? 37740 : 37840);
|
||||
|
||||
portscanner.findAPortNotInUse(startingPort, startingPort + 10, '127.0.0.1', function(error, port) {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
else {
|
||||
resolve(port);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
else {
|
||||
module.exports = Promise.resolve(config['Network']['port'] || '3000');
|
||||
module.exports = Promise.resolve(environmentPort || config['Network']['port'] || '3000');
|
||||
}
|
||||
+31
-25
@@ -8,7 +8,13 @@ const url = require('url');
|
||||
// this allows to support system proxy
|
||||
|
||||
function exec(opts) {
|
||||
// hack for cases where electron.net does not work but we don't want to set proxy
|
||||
if (opts.proxy === 'noproxy') {
|
||||
opts.proxy = null;
|
||||
}
|
||||
|
||||
const client = getClient(opts);
|
||||
const proxyAgent = getProxyAgent(opts);
|
||||
const parsedTargetUrl = url.parse(opts.url);
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
@@ -24,34 +30,18 @@ function exec(opts) {
|
||||
headers['Authorization'] = `Basic ${token}`;
|
||||
}
|
||||
|
||||
let host = parsedTargetUrl.hostname;
|
||||
let protocol = parsedTargetUrl.protocol;
|
||||
let port = parsedTargetUrl.port;
|
||||
let path = parsedTargetUrl.path;
|
||||
|
||||
if (opts.proxy) {
|
||||
// see https://stackoverflow.com/questions/3862813/how-can-i-use-an-http-proxy-with-node-js-http-client
|
||||
const parsedProxyUrl = url.parse(opts.proxy);
|
||||
|
||||
protocol = parsedProxyUrl.protocol;
|
||||
host = parsedProxyUrl.hostname;
|
||||
port = parsedProxyUrl.port;
|
||||
path = opts.url;
|
||||
|
||||
headers['Host'] = parsedTargetUrl.host; // host also includes port
|
||||
}
|
||||
|
||||
const request = client.request({
|
||||
method: opts.method,
|
||||
// url is used by electron net module
|
||||
url: opts.url,
|
||||
// 4 fields below are used by http and https node modules
|
||||
protocol,
|
||||
host,
|
||||
port,
|
||||
path,
|
||||
protocol: parsedTargetUrl.protocol,
|
||||
host: parsedTargetUrl.hostname,
|
||||
port: parsedTargetUrl.port,
|
||||
path: parsedTargetUrl.path,
|
||||
timeout: opts.timeout,
|
||||
headers
|
||||
headers,
|
||||
agent: proxyAgent
|
||||
});
|
||||
|
||||
request.on('error', err => reject(generateError(opts, err)));
|
||||
@@ -91,6 +81,24 @@ function exec(opts) {
|
||||
})
|
||||
}
|
||||
|
||||
function getProxyAgent(opts) {
|
||||
if (!opts.proxy) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {protocol} = url.parse(opts.url);
|
||||
|
||||
if (protocol === 'http:' || protocol === 'https:') {
|
||||
const protoNoColon = protocol.substr(0, protocol.length - 1);
|
||||
const AgentClass = require(protoNoColon + '-proxy-agent');
|
||||
|
||||
return new AgentClass(opts.proxy);
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getClient(opts) {
|
||||
// it's not clear how to explicitly configure proxy (as opposed to system proxy)
|
||||
// so in that case we always use node's modules
|
||||
@@ -98,9 +106,7 @@ function getClient(opts) {
|
||||
return require('electron').net;
|
||||
}
|
||||
else {
|
||||
// in case there's explicit proxy then we need to use protocol of the proxy since we're actually
|
||||
// connecting to the proxy server and not to the end-target server
|
||||
const {protocol} = url.parse(opts.proxy || opts.url);
|
||||
const {protocol} = url.parse(opts.url);
|
||||
|
||||
if (protocol === 'http:' || protocol === 'https:') {
|
||||
return require(protocol.substr(0, protocol.length - 1));
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
const test = require('tape');
|
||||
const data_encryption = require('../services/data_encryption');
|
||||
|
||||
test('encrypt & decrypt', t => {
|
||||
const dataKey = [1,2,3];
|
||||
const iv = [4,5,6];
|
||||
const plainText = "Hello World!";
|
||||
|
||||
const cipherText = data_encryption.encrypt(dataKey, iv, plainText);
|
||||
const decodedPlainText = data_encryption.decrypt(dataKey, iv, cipherText);
|
||||
|
||||
t.equal(decodedPlainText, plainText);
|
||||
t.end();
|
||||
});
|
||||
@@ -200,7 +200,7 @@
|
||||
|
||||
<!-- Include Fancytree skin and library -->
|
||||
<link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet">
|
||||
<script src="libraries/fancytree/jquery.fancytree-all-deps.min.js"></script>
|
||||
<script src="libraries/fancytree/jquery.fancytree-all-deps.js"></script>
|
||||
|
||||
<script src="libraries/jquery.hotkeys.js"></script>
|
||||
<script src="libraries/jquery.fancytree.hotkeys.js"></script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Export note</h5>
|
||||
<h5 class="modal-title">Export note "<span class="export-note-title"></span>"</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
|
||||
@@ -36,9 +36,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0;">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<div style="display: inline-block;">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0;">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
|
||||
<button class="help-button" type="button" data-help-page="Link-map" title="Help on Link map">?</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body" style="outline: none; overflow: hidden;">
|
||||
<div id="link-map-container"></div>
|
||||
|
||||
+3
-3
@@ -72,7 +72,7 @@
|
||||
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
||||
}
|
||||
</script>
|
||||
|
||||
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
</body>
|
||||
|
||||
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
</body>
|
||||
</html>
|
||||
@@ -49,9 +49,13 @@
|
||||
</div>
|
||||
|
||||
<div class="note-detail-component-wrapper">
|
||||
<div class="note-detail-text note-detail-component" tabindex="10000"></div>
|
||||
<div class="note-detail-text note-detail-component" tabindex="10000">
|
||||
<div class="note-detail-text-editor"></div>
|
||||
</div>
|
||||
|
||||
<div class="note-detail-code note-detail-component"></div>
|
||||
<div class="note-detail-code note-detail-component">
|
||||
<div class="note-detail-code-editor"></div>
|
||||
</div>
|
||||
|
||||
<% include details/search.ejs %>
|
||||
|
||||
|
||||
+8
-4
@@ -1,4 +1,4 @@
|
||||
<div class="note-tab-row">
|
||||
<div class="note-tab-row hide-toggle">
|
||||
<div class="note-tab-row-content"></div>
|
||||
</div>
|
||||
|
||||
@@ -11,9 +11,13 @@
|
||||
<table class="note-detail-promoted-attributes"></table>
|
||||
|
||||
<div class="note-detail-component-wrapper">
|
||||
<div class="note-detail-text note-detail-component" tabindex="10000"></div>
|
||||
<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>
|
||||
<div class="note-detail-code note-detail-component">
|
||||
<div class="note-detail-code-editor"></div>
|
||||
</div>
|
||||
|
||||
<% include details/empty.ejs %>
|
||||
|
||||
@@ -29,7 +33,7 @@
|
||||
|
||||
<% include details/protected_session_password.ejs %>
|
||||
|
||||
<div class="children-overview"></div>
|
||||
<div class="children-overview hide-toggle"></div>
|
||||
</div>
|
||||
|
||||
<div class="attribute-list">
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
<a class="dropdown-item show-source-button" data-bind="css: { disabled: type() != 'text' && type() != 'code' && type() != 'relation-map' && type() != 'search' }">Note source</a>
|
||||
<a class="dropdown-item import-files-button">Import files</a>
|
||||
<a class="dropdown-item export-note-button" data-bind="css: { disabled: type() != 'text' }">Export note</a>
|
||||
<a class="dropdown-item print-note-button">Print note</a>
|
||||
<a class="dropdown-item show-note-info-button">Note info</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,7 @@ const messagingService = require('./services/messaging');
|
||||
const utils = require('./services/utils');
|
||||
const sqlInit = require('./services/sql_init');
|
||||
const port = require('./services/port');
|
||||
const host = require('./services/host');
|
||||
const semver = require('semver');
|
||||
|
||||
if (!semver.satisfies(process.version, ">=10.5.0")) {
|
||||
@@ -36,8 +37,10 @@ let httpServer;
|
||||
|
||||
async function startTrilium() {
|
||||
const usedPort = await port;
|
||||
const usedHost = await host;
|
||||
|
||||
app.set('port', usedPort);
|
||||
app.set('host', usedHost);
|
||||
|
||||
if (config['Network']['https']) {
|
||||
if (!config['Network']['keyPath'] || !config['Network']['keyPath'].trim().length) {
|
||||
@@ -70,7 +73,7 @@ async function startTrilium() {
|
||||
*/
|
||||
|
||||
httpServer.keepAliveTimeout = 120000 * 5;
|
||||
httpServer.listen(usedPort);
|
||||
httpServer.listen(usedPort, usedHost);
|
||||
httpServer.on('error', onError);
|
||||
httpServer.on('listening', () => debug('Listening on port' + httpServer.address().port));
|
||||
|
||||
@@ -108,4 +111,4 @@ function onError(error) {
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user