mirror of
https://github.com/zadam/trilium.git
synced 2025-10-27 00:06:30 +01:00
Compare commits
57 Commits
v0.33.4
...
v0.34.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
3851bedb57 | ||
|
|
174128447b | ||
|
|
5d213eea7e | ||
|
|
f45e25172b | ||
|
|
ec87856ef4 | ||
|
|
6feb7ad1d5 | ||
|
|
6833e84d55 | ||
|
|
154a575701 |
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/workspace.xml
|
||||||
|
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources.local.xml
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:12.4.0-alpine
|
FROM node:12.6.0-alpine
|
||||||
|
|
||||||
# Create app directory
|
# Create app directory
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Trilium Notes is a hierarchical note taking application with focus on building l
|
|||||||
* Touch optimized [mobile frontend](https://github.com/zadam/trilium/wiki/Mobile-frontend) for smartphones and tablets
|
* 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)
|
* [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)
|
* [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
|
## Builds
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
PKG_DIR=dist/trilium-linux-x64-server
|
PKG_DIR=dist/trilium-linux-x64-server
|
||||||
NODE_VERSION=12.4.0
|
NODE_VERSION=12.6.0
|
||||||
|
|
||||||
rm -r $PKG_DIR
|
rm -r $PKG_DIR
|
||||||
mkdir $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/
|
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
|
chmod 755 trilium.sh
|
||||||
|
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
VERSION=`jq -r ".version" ../package.json`
|
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
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
export GITHUB_REPO=trilium
|
||||||
|
|
||||||
if [[ $# -eq 0 ]] ; then
|
if [[ $# -eq 0 ]] ; then
|
||||||
echo "Missing argument of new version"
|
echo "Missing argument of new version"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
instanceName=
|
instanceName=
|
||||||
|
|
||||||
[Network]
|
[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 setting is relevant only for web deployments, desktop builds run on random free port
|
||||||
port=8080
|
port=8080
|
||||||
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).
|
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).
|
||||||
|
|||||||
21
electron.js
21
electron.js
@@ -9,6 +9,7 @@ const url = require("url");
|
|||||||
const port = require('./src/services/port');
|
const port = require('./src/services/port');
|
||||||
const appIconService = require('./src/services/app_icon');
|
const appIconService = require('./src/services/app_icon');
|
||||||
const windowStateKeeper = require('electron-window-state');
|
const windowStateKeeper = require('electron-window-state');
|
||||||
|
const contextMenu = require('electron-context-menu');
|
||||||
|
|
||||||
const app = electron.app;
|
const app = electron.app;
|
||||||
const globalShortcut = electron.globalShortcut;
|
const globalShortcut = electron.globalShortcut;
|
||||||
@@ -23,6 +24,26 @@ let mainWindow;
|
|||||||
|
|
||||||
require('electron-dl')({ saveAs: true });
|
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() {
|
function onClosed() {
|
||||||
// Dereference the window
|
// Dereference the window
|
||||||
// For multiple windows store them in an array
|
// For multiple windows store them in an array
|
||||||
|
|||||||
2
libraries/ckeditor/ckeditor.js
vendored
2
libraries/ckeditor/ckeditor.js
vendored
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 {
|
.CodeMirror {
|
||||||
/* Set height, width, borders, and global font properties here */
|
/* Set height, width, borders, and global font properties here */
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
height: 300px;
|
height: auto;
|
||||||
color: black;
|
color: black;
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
}
|
}
|
||||||
|
|||||||
335
libraries/printThis.js
Executable file
335
libraries/printThis.js
Executable file
@@ -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);
|
||||||
1761
package-lock.json
generated
1761
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@@ -2,7 +2,7 @@
|
|||||||
"name": "trilium",
|
"name": "trilium",
|
||||||
"productName": "Trilium Notes",
|
"productName": "Trilium Notes",
|
||||||
"description": "Trilium Notes",
|
"description": "Trilium Notes",
|
||||||
"version": "0.33.4",
|
"version": "0.34.0-beta",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"main": "electron.js",
|
"main": "electron.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
"url": "https://github.com/zadam/trilium.git"
|
"url": "https://github.com/zadam/trilium.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./src/www",
|
"start-server": "TRILIUM_ENV=dev node ./src/www",
|
||||||
"start-electron": "electron . --disable-gpu",
|
"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-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-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",
|
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs",
|
||||||
@@ -28,23 +28,23 @@
|
|||||||
"commonmark": "0.29.0",
|
"commonmark": "0.29.0",
|
||||||
"cookie-parser": "1.4.4",
|
"cookie-parser": "1.4.4",
|
||||||
"csurf": "1.10.0",
|
"csurf": "1.10.0",
|
||||||
"dayjs": "1.8.14",
|
"dayjs": "1.8.15",
|
||||||
"debug": "4.1.1",
|
"debug": "4.1.1",
|
||||||
"ejs": "2.6.2",
|
"ejs": "2.6.2",
|
||||||
"electron-debug": "3.0.0",
|
"electron-context-menu": "0.13.0",
|
||||||
|
"electron-debug": "3.0.1",
|
||||||
"electron-dl": "1.14.0",
|
"electron-dl": "1.14.0",
|
||||||
"electron-find": "1.0.6",
|
"electron-find": "1.0.6",
|
||||||
"electron-window-state": "5.0.3",
|
"electron-window-state": "5.0.3",
|
||||||
"express": "4.17.1",
|
"express": "4.17.1",
|
||||||
"express-session": "1.16.2",
|
"express-session": "1.16.2",
|
||||||
"file-type": "12.0.0",
|
"file-type": "12.0.1",
|
||||||
"fs-extra": "8.0.1",
|
"fs-extra": "8.1.0",
|
||||||
"get-port": "5.0.0",
|
"helmet": "3.19.0",
|
||||||
"helmet": "3.18.0",
|
|
||||||
"html": "1.0.0",
|
"html": "1.0.0",
|
||||||
"html2plaintext": "2.1.2",
|
"html2plaintext": "2.1.2",
|
||||||
"image-type": "4.1.0",
|
"image-type": "4.1.0",
|
||||||
"imagemin": "6.1.0",
|
"imagemin": "7.0.0",
|
||||||
"imagemin-giflossy": "5.1.10",
|
"imagemin-giflossy": "5.1.10",
|
||||||
"imagemin-mozjpeg": "8.0.0",
|
"imagemin-mozjpeg": "8.0.0",
|
||||||
"imagemin-pngquant": "8.0.0",
|
"imagemin-pngquant": "8.0.0",
|
||||||
@@ -52,37 +52,37 @@
|
|||||||
"jimp": "0.6.4",
|
"jimp": "0.6.4",
|
||||||
"mime-types": "2.1.24",
|
"mime-types": "2.1.24",
|
||||||
"moment": "2.24.0",
|
"moment": "2.24.0",
|
||||||
"multer": "1.4.1",
|
"multer": "1.4.2",
|
||||||
"node-abi": "2.9.0",
|
"node-abi": "2.9.0",
|
||||||
"open": "6.3.0",
|
"open": "6.4.0",
|
||||||
"pngjs": "3.4.0",
|
"pngjs": "3.4.0",
|
||||||
|
"portscanner": "2.2.0",
|
||||||
"rand-token": "0.4.0",
|
"rand-token": "0.4.0",
|
||||||
"rcedit": "2.0.0",
|
"rcedit": "2.0.0",
|
||||||
"rimraf": "2.6.3",
|
"rimraf": "2.6.3",
|
||||||
"sanitize-filename": "1.6.1",
|
"sanitize-filename": "1.6.1",
|
||||||
"sax": "1.2.4",
|
"sax": "1.2.4",
|
||||||
"semver": "6.1.1",
|
"semver": "6.2.0",
|
||||||
"serve-favicon": "2.5.0",
|
"serve-favicon": "2.5.0",
|
||||||
"session-file-store": "1.3.0",
|
"session-file-store": "1.3.1",
|
||||||
"simple-node-logger": "18.12.22",
|
"simple-node-logger": "18.12.23",
|
||||||
"sqlite": "3.0.3",
|
"sqlite": "3.0.3",
|
||||||
"sqlite3": "4.0.9",
|
"sqlite3": "4.0.9",
|
||||||
"tar-stream": "2.1.0",
|
"tar-stream": "2.1.0",
|
||||||
"turndown": "5.0.3",
|
"turndown": "5.0.3",
|
||||||
"unescape": "1.0.1",
|
"unescape": "1.0.1",
|
||||||
"ws": "7.0.1",
|
"ws": "7.1.0",
|
||||||
"xml2js": "0.4.19"
|
"xml2js": "0.4.19"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"devtron": "1.4.0",
|
"devtron": "1.4.0",
|
||||||
"electron": "6.0.0-beta.11",
|
"electron": "6.0.0-beta.14",
|
||||||
"electron-builder": "20.44.2",
|
"electron-builder": "21.1.1",
|
||||||
"electron-compile": "6.4.4",
|
"electron-compile": "6.4.4",
|
||||||
"electron-installer-debian": "2.0.0",
|
"electron-installer-debian": "2.0.0",
|
||||||
"electron-packager": "13.1.1",
|
"electron-packager": "14.0.2",
|
||||||
"electron-rebuild": "1.8.5",
|
"electron-rebuild": "1.8.5",
|
||||||
"lorem-ipsum": "2.0.3",
|
"lorem-ipsum": "2.0.3",
|
||||||
"tape": "4.10.2",
|
|
||||||
"xo": "0.24.0"
|
"xo": "0.24.0"
|
||||||
},
|
},
|
||||||
"xo": {
|
"xo": {
|
||||||
|
|||||||
@@ -147,6 +147,26 @@ $noteTabContainer.on("click", ".export-note-button", function () {
|
|||||||
|
|
||||||
$noteTabContainer.on("click", ".import-files-button", () => importDialog.showDialog(treeService.getActiveNode()));
|
$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({
|
$('[data-toggle="tooltip"]').tooltip({
|
||||||
html: true
|
html: true
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ const $custom = $("#confirm-dialog-custom");
|
|||||||
const DELETE_NOTE_BUTTON_ID = "confirm-dialog-delete-note";
|
const DELETE_NOTE_BUTTON_ID = "confirm-dialog-delete-note";
|
||||||
|
|
||||||
let resolve;
|
let resolve;
|
||||||
|
let $originallyFocused; // element focused before the dialog was opened so we can return to it afterwards
|
||||||
|
|
||||||
function confirm(message) {
|
function confirm(message) {
|
||||||
|
$originallyFocused = $(':focus');
|
||||||
|
|
||||||
$custom.hide();
|
$custom.hide();
|
||||||
|
|
||||||
glob.activeDialog = $dialog;
|
glob.activeDialog = $dialog;
|
||||||
@@ -55,6 +58,11 @@ $dialog.on("hidden.bs.modal", () => {
|
|||||||
if (resolve) {
|
if (resolve) {
|
||||||
resolve(false);
|
resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($originallyFocused) {
|
||||||
|
$originallyFocused.focus();
|
||||||
|
$originallyFocused = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function doResolve(ret) {
|
function doResolve(ret) {
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ const $infoContent = $("#info-dialog-content");
|
|||||||
const $okButton = $("#info-dialog-ok-button");
|
const $okButton = $("#info-dialog-ok-button");
|
||||||
|
|
||||||
let resolve;
|
let resolve;
|
||||||
|
let $originallyFocused; // element focused before the dialog was opened so we can return to it afterwards
|
||||||
|
|
||||||
function info(message) {
|
function info(message) {
|
||||||
|
$originallyFocused = $(':focus');
|
||||||
|
|
||||||
utils.closeActiveDialog();
|
utils.closeActiveDialog();
|
||||||
|
|
||||||
glob.activeDialog = $dialog;
|
glob.activeDialog = $dialog;
|
||||||
@@ -24,6 +27,11 @@ $dialog.on("hidden.bs.modal", () => {
|
|||||||
if (resolve) {
|
if (resolve) {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($originallyFocused) {
|
||||||
|
$originallyFocused.focus();
|
||||||
|
$originallyFocused = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$okButton.click(() => $dialog.modal("hide"));
|
$okButton.click(() => $dialog.modal("hide"));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import treeCache from "./tree_cache.js";
|
|||||||
import treeUtils from "./tree_utils.js";
|
import treeUtils from "./tree_utils.js";
|
||||||
import hoistedNoteService from "./hoisted_note.js";
|
import hoistedNoteService from "./hoisted_note.js";
|
||||||
import noteDetailService from "./note_detail.js";
|
import noteDetailService from "./note_detail.js";
|
||||||
|
import confirmDialog from "../dialogs/confirm.js";
|
||||||
|
|
||||||
async function moveBeforeNode(nodesToMove, beforeNode) {
|
async function moveBeforeNode(nodesToMove, beforeNode) {
|
||||||
nodesToMove = await filterRootNote(nodesToMove);
|
nodesToMove = await filterRootNote(nodesToMove);
|
||||||
@@ -82,7 +83,7 @@ async function moveToNode(nodesToMove, toNode) {
|
|||||||
async function deleteNodes(nodes) {
|
async function deleteNodes(nodes) {
|
||||||
nodes = await filterRootNote(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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +103,7 @@ async function deleteNodes(nodes) {
|
|||||||
next = nodes[0].getPrevSibling();
|
next = nodes[0].getPrevSibling();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!next && !hoistedNoteService.isTopLevelNode(nodes[0])) {
|
if (!next && !await hoistedNoteService.isTopLevelNode(nodes[0])) {
|
||||||
next = nodes[0].getParent();
|
next = nodes[0].getParent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ const LINK_MAP = {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PRINT_THIS = {js: ["libraries/printThis.js"]};
|
||||||
|
|
||||||
async function requireLibrary(library) {
|
async function requireLibrary(library) {
|
||||||
if (library.css) {
|
if (library.css) {
|
||||||
library.css.map(cssUrl => cssLoader.requireCss(cssUrl));
|
library.css.map(cssUrl => cssLoader.requireCss(cssUrl));
|
||||||
@@ -79,5 +81,6 @@ export default {
|
|||||||
ESLINT,
|
ESLINT,
|
||||||
COMMONMARK,
|
COMMONMARK,
|
||||||
RELATION_MAP,
|
RELATION_MAP,
|
||||||
LINK_MAP
|
LINK_MAP,
|
||||||
|
PRINT_THIS
|
||||||
}
|
}
|
||||||
@@ -67,12 +67,9 @@ function connectWebSocket() {
|
|||||||
|
|
||||||
// use wss for secure messaging
|
// use wss for secure messaging
|
||||||
const ws = new WebSocket(protocol + "://" + location.host);
|
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.onmessage = handleMessage;
|
||||||
ws.onclose = function(){
|
// we're not handling ws.onclose here because reconnection is done in sendPing()
|
||||||
// Try to reconnect in 5 seconds
|
|
||||||
setTimeout(() => connectWebSocket(), 5000);
|
|
||||||
};
|
|
||||||
|
|
||||||
return ws;
|
return ws;
|
||||||
}
|
}
|
||||||
@@ -88,13 +85,17 @@ setTimeout(() => {
|
|||||||
console.log("Lost connection to server");
|
console.log("Lost connection to server");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (ws.readyState === ws.OPEN) {
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
type: 'ping',
|
type: 'ping',
|
||||||
lastSyncId: lastSyncId
|
lastSyncId: lastSyncId
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
catch (e) {} // if the connection is closed then this produces a lot of messages
|
else if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
|
||||||
|
console.log("WS closed or closing, trying to reconnect");
|
||||||
|
|
||||||
|
ws = connectWebSocket();
|
||||||
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}, 0);
|
}, 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() {
|
function getTabContexts() {
|
||||||
return tabContexts;
|
return tabContexts;
|
||||||
}
|
}
|
||||||
@@ -544,6 +560,7 @@ export default {
|
|||||||
getTabContexts,
|
getTabContexts,
|
||||||
getActiveTabContext,
|
getActiveTabContext,
|
||||||
getActiveEditor,
|
getActiveEditor,
|
||||||
|
activateOrOpenNote,
|
||||||
clearOpenTabsTask,
|
clearOpenTabsTask,
|
||||||
filterTabs,
|
filterTabs,
|
||||||
openEmptyTab,
|
openEmptyTab,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class NoteDetailCode {
|
|||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
this.codeEditor = null;
|
this.codeEditor = null;
|
||||||
this.$component = ctx.$tabContent.find('.note-detail-code');
|
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");
|
this.$executeScriptButton = ctx.$tabContent.find(".execute-script-button");
|
||||||
|
|
||||||
utils.bindElShortcut(ctx.$tabContent, "ctrl+return", () => this.executeCurrentNote());
|
utils.bindElShortcut(ctx.$tabContent, "ctrl+return", () => this.executeCurrentNote());
|
||||||
@@ -34,7 +35,7 @@ class NoteDetailCode {
|
|||||||
|
|
||||||
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
|
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
|
||||||
|
|
||||||
this.codeEditor = CodeMirror(this.$component[0], {
|
this.codeEditor = CodeMirror(this.$editorEl[0], {
|
||||||
value: "",
|
value: "",
|
||||||
viewportMargin: Infinity,
|
viewportMargin: Infinity,
|
||||||
indentUnit: 4,
|
indentUnit: 4,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
import noteDetailService from "./note_detail.js";
|
|
||||||
import infoService from "./info.js";
|
import infoService from "./info.js";
|
||||||
import server from "./server.js";
|
import server from "./server.js";
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class NoteDetailText {
|
|||||||
constructor(ctx) {
|
constructor(ctx) {
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
this.$component = ctx.$tabContent.find('.note-detail-text');
|
this.$component = ctx.$tabContent.find('.note-detail-text');
|
||||||
|
this.$editorEl = this.$component.find('.note-detail-text-editor');
|
||||||
this.textEditor = null;
|
this.textEditor = null;
|
||||||
|
|
||||||
this.$component.on("dblclick", "img", e => {
|
this.$component.on("dblclick", "img", e => {
|
||||||
@@ -39,7 +40,7 @@ class NoteDetailText {
|
|||||||
// textEditor might have been initialized during previous await so checking again
|
// textEditor might have been initialized during previous await so checking again
|
||||||
// looks like double initialization can freeze CKEditor pretty badly
|
// looks like double initialization can freeze CKEditor pretty badly
|
||||||
if (!this.textEditor) {
|
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 ..."
|
placeholder: "Type the content of your note here ..."
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ class NoteDetailText {
|
|||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
this.$component.focus();
|
this.$editorEl.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
getEditor() {
|
getEditor() {
|
||||||
|
|||||||
@@ -83,7 +83,11 @@ class TabContext {
|
|||||||
|
|
||||||
if (utils.isDesktop()) {
|
if (utils.isDesktop()) {
|
||||||
// keyboard plugin is not loaded in mobile
|
// 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");
|
this.$protectButton = this.$tabContent.find(".protect-button");
|
||||||
@@ -92,7 +96,7 @@ class TabContext {
|
|||||||
this.$unprotectButton = this.$tabContent.find(".unprotect-button");
|
this.$unprotectButton = this.$tabContent.find(".unprotect-button");
|
||||||
this.$unprotectButton.click(protectedSessionService.unprotectNoteAndSendToServer);
|
this.$unprotectButton.click(protectedSessionService.unprotectNoteAndSendToServer);
|
||||||
|
|
||||||
console.log(`Created note tab ${this.tabId}`);
|
console.debug(`Created note tab ${this.tabId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setNote(note, notePath) {
|
setNote(note, notePath) {
|
||||||
@@ -127,7 +131,7 @@ class TabContext {
|
|||||||
|
|
||||||
this.showPaths();
|
this.showPaths();
|
||||||
|
|
||||||
console.log(`Switched tab ${this.tabId} to ${this.noteId}`);
|
console.debug(`Switched tab ${this.tabId} to ${this.noteId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
@@ -338,7 +342,9 @@ class TabContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
closeAutocomplete() {
|
closeAutocomplete() {
|
||||||
this.$tabContent.find('.aa-input').autocomplete('close');
|
if (utils.isDesktop()) {
|
||||||
|
this.$tabContent.find('.aa-input').autocomplete('close');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -196,6 +196,8 @@ async function resolveNotePath(notePath) {
|
|||||||
async function getRunPath(notePath) {
|
async function getRunPath(notePath) {
|
||||||
utils.assertArguments(notePath);
|
utils.assertArguments(notePath);
|
||||||
|
|
||||||
|
notePath = notePath.split("-")[0];
|
||||||
|
|
||||||
const path = notePath.split("/").reverse();
|
const path = notePath.split("/").reverse();
|
||||||
|
|
||||||
if (!path.includes("root")) {
|
if (!path.includes("root")) {
|
||||||
@@ -335,6 +337,31 @@ async function treeInitialized() {
|
|||||||
messagingService.logError("Cannot retrieve open tabs: " + e.stack);
|
messagingService.logError("Cannot retrieve open tabs: " + e.stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const filteredTabs = [];
|
const filteredTabs = [];
|
||||||
|
|
||||||
for (const openTab of openTabs) {
|
for (const openTab of openTabs) {
|
||||||
@@ -630,7 +657,8 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
|
|||||||
extraClasses: await treeBuilder.getExtraClasses(noteEntity),
|
extraClasses: await treeBuilder.getExtraClasses(noteEntity),
|
||||||
icon: await treeBuilder.getIcon(noteEntity),
|
icon: await treeBuilder.getIcon(noteEntity),
|
||||||
folder: extraOptions.type === 'search',
|
folder: extraOptions.type === 'search',
|
||||||
lazy: true
|
lazy: true,
|
||||||
|
key: utils.randomString(12) // this should prevent some "duplicate key" errors
|
||||||
};
|
};
|
||||||
|
|
||||||
if (target === 'after') {
|
if (target === 'after') {
|
||||||
@@ -638,10 +666,14 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
|
|||||||
}
|
}
|
||||||
else if (target === 'into') {
|
else if (target === 'into') {
|
||||||
if (!node.getChildren() && node.isFolder()) {
|
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();
|
await node.setExpanded();
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
node.addChildren(newNode);
|
node.addChildren(newNode);
|
||||||
|
}
|
||||||
|
|
||||||
await node.getLastChild().setActive(true);
|
await node.getLastChild().setActive(true);
|
||||||
|
|
||||||
@@ -707,6 +739,15 @@ messagingService.subscribeToMessages(message => {
|
|||||||
if (message.type === 'refresh-tree') {
|
if (message.type === 'refresh-tree') {
|
||||||
reload();
|
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 => {
|
messagingService.subscribeToSyncMessages(syncData => {
|
||||||
@@ -737,10 +778,12 @@ utils.bindShortcut('ctrl+o', async () => {
|
|||||||
async function createNoteInto() {
|
async function createNoteInto() {
|
||||||
const node = getActiveNode();
|
const node = getActiveNode();
|
||||||
|
|
||||||
await createNote(node, node.data.noteId, 'into', {
|
if (node) {
|
||||||
isProtected: node.data.isProtected,
|
await createNote(node, node.data.noteId, 'into', {
|
||||||
saveSelection: true
|
isProtected: node.data.isProtected,
|
||||||
});
|
saveSelection: true
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkFolderStatus(node) {
|
async function checkFolderStatus(node) {
|
||||||
|
|||||||
@@ -83,7 +83,8 @@ async function prepareNode(branch) {
|
|||||||
icon: await getIcon(note),
|
icon: await getIcon(note),
|
||||||
refKey: note.noteId,
|
refKey: note.noteId,
|
||||||
expanded: branch.isExpanded || hoistedNoteId === 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') {
|
if (note.hasChildren() || note.type === 'search') {
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ function getNoteIdFromNotePath(notePath) {
|
|||||||
|
|
||||||
const path = notePath.split("/");
|
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) {
|
async function getNotePath(node) {
|
||||||
|
|||||||
@@ -135,13 +135,16 @@ ul.fancytree-container {
|
|||||||
.note-detail-text h6 { font-size: 1.1em; }
|
.note-detail-text h6 { font-size: 1.1em; }
|
||||||
|
|
||||||
.note-detail-text {
|
.note-detail-text {
|
||||||
|
overflow: auto;
|
||||||
|
font-family: var(--detail-text-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-text-editor {
|
||||||
|
padding-top: 10px;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
/* This is because with empty content height of editor is 0 and it's impossible to click into it */
|
/* This is because with empty content height of editor is 0 and it's impossible to click into it */
|
||||||
min-height: 200px;
|
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 {
|
.note-detail-text p:first-child, .note-detail-text::before {
|
||||||
@@ -354,10 +357,13 @@ div.ui-tooltip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.note-detail-code {
|
.note-detail-code {
|
||||||
min-height: 200px;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-detail-code-editor {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
.note-detail-render {
|
.note-detail-render {
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
}
|
}
|
||||||
@@ -832,4 +838,8 @@ a.external:after, a[href^="http://"]:after, a[href^="https://"]:after {
|
|||||||
|
|
||||||
.note-detail-empty {
|
.note-detail-empty {
|
||||||
margin: 50px;
|
margin: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 0.7rem 1rem !important; /* make modal header padding slightly smaller */
|
||||||
}
|
}
|
||||||
132
src/routes/api/clipper.js
Normal file
132
src/routes/api/clipper.js
Normal file
@@ -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.`];
|
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];
|
return [400, "Unknown image type: " + file.mimetype];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const eventService = require('../../services/events');
|
|||||||
const cls = require('../../services/cls');
|
const cls = require('../../services/cls');
|
||||||
const sqlInit = require('../../services/sql_init');
|
const sqlInit = require('../../services/sql_init');
|
||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
|
const optionService = require('../../services/options');
|
||||||
|
const ApiToken = require('../../entities/api_token');
|
||||||
|
|
||||||
async function loginSync(req) {
|
async function loginSync(req) {
|
||||||
if (!await sqlInit.schemaExists()) {
|
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 = {
|
module.exports = {
|
||||||
loginSync,
|
loginSync,
|
||||||
loginToProtectedSession
|
loginToProtectedSession,
|
||||||
|
token
|
||||||
};
|
};
|
||||||
@@ -1,33 +1,9 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
const imageType = require('image-type');
|
||||||
const imageService = require('../../services/image');
|
const imageService = require('../../services/image');
|
||||||
const utils = require('../../services/utils');
|
|
||||||
const dateNoteService = require('../../services/date_notes');
|
const dateNoteService = require('../../services/date_notes');
|
||||||
const sql = require('../../services/sql');
|
|
||||||
const noteService = require('../../services/notes');
|
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) {
|
async function uploadImage(req) {
|
||||||
const file = req.file;
|
const file = req.file;
|
||||||
@@ -36,9 +12,11 @@ async function uploadImage(req) {
|
|||||||
return [400, "Unknown image type: " + file.mimetype];
|
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 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 {
|
return {
|
||||||
noteId: noteId
|
noteId: noteId
|
||||||
@@ -64,7 +42,6 @@ async function saveNote(req) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
login,
|
|
||||||
uploadImage,
|
uploadImage,
|
||||||
saveNote
|
saveNote
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
const setupRoute = require('./setup');
|
const setupRoute = require('./setup');
|
||||||
const loginRoute = require('./login');
|
const loginRoute = require('./login');
|
||||||
const indexRoute = require('./index');
|
const indexRoute = require('./index');
|
||||||
|
const utils = require('../services/utils');
|
||||||
const multer = require('multer')();
|
const multer = require('multer')();
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
@@ -32,6 +33,7 @@ const filesRoute = require('./api/file_upload');
|
|||||||
const searchRoute = require('./api/search');
|
const searchRoute = require('./api/search');
|
||||||
const dateNotesRoute = require('./api/date_notes');
|
const dateNotesRoute = require('./api/date_notes');
|
||||||
const linkMapRoute = require('./api/link_map');
|
const linkMapRoute = require('./api/link_map');
|
||||||
|
const clipperRoute = require('./api/clipper');
|
||||||
|
|
||||||
const log = require('../services/log');
|
const log = require('../services/log');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
@@ -212,9 +214,9 @@ function register(app) {
|
|||||||
apiRoute(GET, '/api/script/relation/:noteId/:relationName', scriptRoute.getRelationBundles);
|
apiRoute(GET, '/api/script/relation/:noteId/:relationName', scriptRoute.getRelationBundles);
|
||||||
|
|
||||||
// no CSRF since this is called from android app
|
// no CSRF since this is called from android app
|
||||||
route(POST, '/api/sender/login', [], senderRoute.login, apiResultHandler);
|
route(POST, '/api/sender/login', [], loginApiRoute.token, apiResultHandler);
|
||||||
route(POST, '/api/sender/image', [auth.checkSenderToken, uploadMiddleware], senderRoute.uploadImage, apiResultHandler);
|
route(POST, '/api/sender/image', [auth.checkToken, uploadMiddleware], senderRoute.uploadImage, apiResultHandler);
|
||||||
route(POST, '/api/sender/note', [auth.checkSenderToken], senderRoute.saveNote, apiResultHandler);
|
route(POST, '/api/sender/note', [auth.checkToken], senderRoute.saveNote, apiResultHandler);
|
||||||
|
|
||||||
apiRoute(GET, '/api/search/:searchString', searchRoute.searchNotes);
|
apiRoute(GET, '/api/search/:searchString', searchRoute.searchNotes);
|
||||||
apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote);
|
apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote);
|
||||||
@@ -222,6 +224,15 @@ function register(app) {
|
|||||||
route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler);
|
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)
|
// 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);
|
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);
|
app.use('', router);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const {TRILIUM_DATA_DIR} = require('./data_dir');
|
|||||||
|
|
||||||
const APP_DB_VERSION = 136;
|
const APP_DB_VERSION = 136;
|
||||||
const SYNC_VERSION = 9;
|
const SYNC_VERSION = 9;
|
||||||
|
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
appVersion: packageJson.version,
|
appVersion: packageJson.version,
|
||||||
@@ -13,5 +14,6 @@ module.exports = {
|
|||||||
syncVersion: SYNC_VERSION,
|
syncVersion: SYNC_VERSION,
|
||||||
buildDate: build.buildDate,
|
buildDate: build.buildDate,
|
||||||
buildRevision: build.buildRevision,
|
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;
|
const token = req.headers.authorization;
|
||||||
|
|
||||||
if (await sql.getValue("SELECT COUNT(*) FROM api_tokens WHERE isDeleted = 0 AND token = ?", [token]) === 0) {
|
if (await sql.getValue("SELECT COUNT(*) FROM api_tokens WHERE isDeleted = 0 AND token = ?", [token]) === 0) {
|
||||||
@@ -89,6 +89,6 @@ module.exports = {
|
|||||||
checkAppInitialized,
|
checkAppInitialized,
|
||||||
checkAppNotInitialized,
|
checkAppNotInitialized,
|
||||||
checkApiAuthOrElectron,
|
checkApiAuthOrElectron,
|
||||||
checkSenderToken,
|
checkToken,
|
||||||
checkBasicAuth
|
checkBasicAuth
|
||||||
};
|
};
|
||||||
@@ -1 +1 @@
|
|||||||
module.exports = { buildDate:"2019-06-26T21:20:30+02:00", buildRevision: "a3951f1cce978699d81f312c590d71f6ca4c6771" };
|
module.exports = { buildDate:"2019-07-21T21:57:35+02:00", buildRevision: "6d5f8e056263dbaaf3abf2d532a4fd1059745b58" };
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ async function getNoteStartingWith(parentNoteId, startsWith) {
|
|||||||
AND branches.isDeleted = 0`, [parentNoteId]);
|
AND branches.isDeleted = 0`, [parentNoteId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return {Promise<Note>} */
|
||||||
async function getRootCalendarNote() {
|
async function getRootCalendarNote() {
|
||||||
// some caching here could be useful (e.g. in CLS)
|
// some caching here could be useful (e.g. in CLS)
|
||||||
let rootNote = await attributeService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
|
let rootNote = await attributeService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
|
||||||
@@ -47,6 +48,7 @@ async function getRootCalendarNote() {
|
|||||||
return rootNote;
|
return rootNote;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return {Promise<Note>} */
|
||||||
async function getYearNote(dateStr, rootNote) {
|
async function getYearNote(dateStr, rootNote) {
|
||||||
if (!rootNote) {
|
if (!rootNote) {
|
||||||
rootNote = await getRootCalendarNote();
|
rootNote = await getRootCalendarNote();
|
||||||
@@ -79,6 +81,7 @@ async function getMonthNoteTitle(rootNote, monthNumber, dateObj) {
|
|||||||
.replace(/{month}/g, monthName);
|
.replace(/{month}/g, monthName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return {Promise<Note>} */
|
||||||
async function getMonthNote(dateStr, rootNote) {
|
async function getMonthNote(dateStr, rootNote) {
|
||||||
const monthStr = dateStr.substr(0, 7);
|
const monthStr = dateStr.substr(0, 7);
|
||||||
const monthNumber = dateStr.substr(5, 2);
|
const monthNumber = dateStr.substr(5, 2);
|
||||||
@@ -116,6 +119,7 @@ async function getDateNoteTitle(rootNote, dayNumber, dateObj) {
|
|||||||
.replace(/{weekDay2}/g, weekDay.substr(0, 2));
|
.replace(/{weekDay2}/g, weekDay.substr(0, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return {Promise<Note>} */
|
||||||
async function getDateNote(dateStr) {
|
async function getDateNote(dateStr) {
|
||||||
const rootNote = await getRootCalendarNote();
|
const rootNote = await getRootCalendarNote();
|
||||||
|
|
||||||
|
|||||||
5
src/services/env.js
Normal file
5
src/services/env.js
Normal file
@@ -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 {
|
do {
|
||||||
index = existingFileNames[lcFileName]++;
|
index = existingFileNames[lcFileName]++;
|
||||||
|
|
||||||
newName = lcFileName + "_" + index;
|
newName = index + "_" + lcFileName;
|
||||||
}
|
}
|
||||||
while (newName in existingFileNames);
|
while (newName in existingFileNames);
|
||||||
|
|
||||||
return fileName + "_" + index;
|
return index + "_" + fileName;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
existingFileNames[lcFileName] = 1;
|
existingFileNames[lcFileName] = 1;
|
||||||
@@ -46,24 +46,32 @@ async function exportToTar(exportContext, branch, format, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDataFileName(note, baseFileName, existingFileNames) {
|
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') {
|
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') {
|
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 {
|
else {
|
||||||
extension = mimeTypes.extension(note.mime) || "dat";
|
newExtension = mimeTypes.extension(note.mime) || "dat";
|
||||||
}
|
}
|
||||||
|
|
||||||
let fileName = baseFileName;
|
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 the note is already named with extension (e.g. "jquery.js"), then it's silly to append exact same extension again
|
||||||
if (existingExtension !== extension) {
|
if (newExtension && existingExtension !== "." + newExtension.toLowerCase()) {
|
||||||
fileName += "." + extension;
|
fileName += "." + newExtension;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getUniqueFilename(existingFileNames, fileName);
|
return getUniqueFilename(existingFileNames, fileName);
|
||||||
|
|||||||
10
src/services/host.js
Normal file
10
src/services/host.js
Normal file
@@ -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');
|
||||||
@@ -13,14 +13,20 @@ const imageType = require('image-type');
|
|||||||
const sanitizeFilename = require('sanitize-filename');
|
const sanitizeFilename = require('sanitize-filename');
|
||||||
|
|
||||||
async function saveImage(buffer, originalName, parentNoteId, shrinkImageSwitch) {
|
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 finalImageBuffer = shrinkImageSwitch ? await shrinkImage(buffer, originalName) : buffer;
|
||||||
|
|
||||||
const imageFormat = imageType(finalImageBuffer);
|
const imageFormat = imageType(finalImageBuffer);
|
||||||
|
|
||||||
const parentNote = await repository.getNote(parentNoteId);
|
const parentNote = await repository.getNote(parentNoteId);
|
||||||
|
|
||||||
const fileNameWithoutExtension = originalName.replace(/\.[^/.]+$/, "");
|
const fileName = sanitizeFilename(originalName);
|
||||||
const fileName = sanitizeFilename(fileNameWithoutExtension + "." + imageFormat.ext);
|
|
||||||
|
|
||||||
const {note} = await noteService.createNote(parentNoteId, fileName, finalImageBuffer, {
|
const {note} = await noteService.createNote(parentNoteId, fileName, finalImageBuffer, {
|
||||||
target: 'into',
|
target: 'into',
|
||||||
@@ -48,7 +54,7 @@ async function shrinkImage(buffer, originalName) {
|
|||||||
try {
|
try {
|
||||||
finalImageBuffer = await optimize(resizedImage);
|
finalImageBuffer = await optimize(resizedImage);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("Failed to optimize image '" + originalName + "\nStack: " + e.stack);
|
log.error("Failed to optimize image '" + originalName + "'\nStack: " + e.stack);
|
||||||
finalImageBuffer = resizedImage;
|
finalImageBuffer = resizedImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +99,7 @@ async function optimize(buffer) {
|
|||||||
quality: 50
|
quality: 50
|
||||||
}),
|
}),
|
||||||
imageminPngQuant({
|
imageminPngQuant({
|
||||||
quality: "0-70"
|
quality: [0, 0.7]
|
||||||
}),
|
}),
|
||||||
imageminGifLossy({
|
imageminGifLossy({
|
||||||
lossy: 80,
|
lossy: 80,
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ async function importEnex(importContext, file, parentNote) {
|
|||||||
noteContent = noteContent.replace(mediaRegex, resourceLink);
|
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 {
|
try {
|
||||||
const originalName = "image." + resource.mime.substr(6);
|
const originalName = "image." + resource.mime.substr(6);
|
||||||
|
|
||||||
|
|||||||
@@ -75,8 +75,6 @@ function getMime(fileName) {
|
|||||||
const ext = path.extname(fileName).toLowerCase();
|
const ext = path.extname(fileName).toLowerCase();
|
||||||
|
|
||||||
if (ext in EXTENSION_TO_MIME) {
|
if (ext in EXTENSION_TO_MIME) {
|
||||||
console.log(EXTENSION_TO_MIME[ext]);
|
|
||||||
|
|
||||||
return EXTENSION_TO_MIME[ext];
|
return EXTENSION_TO_MIME[ext];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +98,7 @@ async function importSingleFile(importContext, file, parentNote) {
|
|||||||
return await importCodeNote(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);
|
return await importImage(file, parentNote, importContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +106,7 @@ async function importSingleFile(importContext, file, parentNote) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function importImage(file, parentNote, importContext) {
|
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();
|
importContext.increaseProgressCount();
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,28 @@
|
|||||||
const getPort = require('get-port');
|
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
const utils = require('./utils');
|
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()) {
|
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 {
|
else {
|
||||||
module.exports = Promise.resolve(config['Network']['port'] || '3000');
|
module.exports = Promise.resolve(environmentPort || config['Network']['port'] || '3000');
|
||||||
}
|
}
|
||||||
@@ -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();
|
|
||||||
});
|
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -49,7 +49,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="note-detail-component-wrapper">
|
<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>
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,13 @@
|
|||||||
<table class="note-detail-promoted-attributes"></table>
|
<table class="note-detail-promoted-attributes"></table>
|
||||||
|
|
||||||
<div class="note-detail-component-wrapper">
|
<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 %>
|
<% include details/empty.ejs %>
|
||||||
|
|
||||||
|
|||||||
@@ -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 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 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 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>
|
<a class="dropdown-item show-note-info-button">Note info</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
7
src/www
7
src/www
@@ -25,6 +25,7 @@ const messagingService = require('./services/messaging');
|
|||||||
const utils = require('./services/utils');
|
const utils = require('./services/utils');
|
||||||
const sqlInit = require('./services/sql_init');
|
const sqlInit = require('./services/sql_init');
|
||||||
const port = require('./services/port');
|
const port = require('./services/port');
|
||||||
|
const host = require('./services/host');
|
||||||
const semver = require('semver');
|
const semver = require('semver');
|
||||||
|
|
||||||
if (!semver.satisfies(process.version, ">=10.5.0")) {
|
if (!semver.satisfies(process.version, ">=10.5.0")) {
|
||||||
@@ -36,8 +37,10 @@ let httpServer;
|
|||||||
|
|
||||||
async function startTrilium() {
|
async function startTrilium() {
|
||||||
const usedPort = await port;
|
const usedPort = await port;
|
||||||
|
const usedHost = await host;
|
||||||
|
|
||||||
app.set('port', usedPort);
|
app.set('port', usedPort);
|
||||||
|
app.set('host', usedHost);
|
||||||
|
|
||||||
if (config['Network']['https']) {
|
if (config['Network']['https']) {
|
||||||
if (!config['Network']['keyPath'] || !config['Network']['keyPath'].trim().length) {
|
if (!config['Network']['keyPath'] || !config['Network']['keyPath'].trim().length) {
|
||||||
@@ -70,7 +73,7 @@ async function startTrilium() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
httpServer.keepAliveTimeout = 120000 * 5;
|
httpServer.keepAliveTimeout = 120000 * 5;
|
||||||
httpServer.listen(usedPort);
|
httpServer.listen(usedPort, usedHost);
|
||||||
httpServer.on('error', onError);
|
httpServer.on('error', onError);
|
||||||
httpServer.on('listening', () => debug('Listening on port' + httpServer.address().port));
|
httpServer.on('listening', () => debug('Listening on port' + httpServer.address().port));
|
||||||
|
|
||||||
@@ -108,4 +111,4 @@ function onError(error) {
|
|||||||
default:
|
default:
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user