Compare commits

...

57 Commits

Author SHA1 Message Date
zadam
b0c0c5f56b release 0.34.0-beta 2019-07-21 21:57:35 +02:00
zadam
6d5f8e0562 added web clipper to readme 2019-07-20 14:27:36 +02:00
zadam
04633bdf3a set clip type from the incoming param 2019-07-19 21:03:47 +02:00
zadam
97219aa12e removed unused clipper image API (migrated to clippings) 2019-07-18 22:35:16 +02:00
zadam
e825abf893 Merge remote-tracking branch 'origin/master' 2019-07-18 20:20:45 +02:00
zadam
63b655cff4 package updates 2019-07-18 20:20:38 +02:00
Yevhen Kolomeiko
0524942d11 fix typo (#592) 2019-07-12 07:16:37 +02:00
zadam
b5d75f183a update to ckeditor 12.3.1 2019-07-11 22:30:40 +02:00
zadam
706fc647ff Merge branch 'stable'
# Conflicts:
#	package.json
2019-07-11 22:27:27 +02:00
zadam
13e9f9f9e7 release 0.33.7 2019-07-11 20:55:56 +02:00
zadam
a76dcb44ae improvements in the exported file extensions 2019-07-11 20:54:57 +02:00
zadam
b9373806cf fix doubling of extension 2019-07-11 20:40:40 +02:00
zadam
9de2927304 image import/export related fixes 2019-07-10 23:01:30 +02:00
zadam
c3e1126489 backported image fixes 2019-07-10 20:38:27 +02:00
zadam
3413c9ed64 backport notePath handling fixes from master 2019-07-10 20:35:01 +02:00
Xavier NUNN
dcebcb0e73 Add host option (#588)
* Added option to configure host

* Updated sample config
2019-07-09 22:50:20 +02:00
zadam
3d7a5f20e7 make it again possible to open notes through URL hash 2019-07-09 22:12:05 +02:00
zadam
6a99af64a8 added clipper protocol version 2019-07-07 22:27:06 +02:00
zadam
7d57961ab2 make clipper api authenticated for server and unauthenticated for local electron 2019-07-07 13:12:40 +02:00
zadam
95a773e5c9 clipper doesn't open new tab if the note is already loaded in existing tab 2019-07-07 11:15:55 +02:00
zadam
a912b2f23d choose port dynamically from range based on environment 2019-07-07 10:49:34 +02:00
zadam
36b581489c saving selections adds to the existing date note instead of creating new one each time 2019-07-06 23:54:48 +02:00
zadam
976684a3a8 save note in clipper 2019-07-06 16:48:06 +02:00
zadam
093dfb4a39 Merge branch 'stable' 2019-07-06 13:14:46 +02:00
zadam
ddf381f92d fixed duplicated notes after creating into a folder which wasn't yet loaded 2019-07-06 13:14:33 +02:00
zadam
2b44f3bc76 fixed websocket reconnection 2019-07-06 12:03:51 +02:00
zadam
7b1fdfabf8 upgrade ckeditor to 12.3.0 2019-07-04 19:53:10 +02:00
zadam
070e8d9647 Merge branch 'stable' 2019-07-03 20:51:34 +02:00
zadam
bf3360572a nest code editor instance to avoid visibility issues 2019-07-03 20:37:59 +02:00
zadam
e5036318af fix enter on title to the code editor 2019-07-03 20:29:55 +02:00
zadam
6d2394a9da release 0.33.6 2019-07-02 22:26:05 +02:00
zadam
427a266c57 Merge branch 'stable' 2019-07-02 21:56:23 +02:00
zadam
196264b8c2 use bootstrap modal to confirm note deletion which fixes #582 2019-07-02 21:54:37 +02:00
zadam
afe24866f0 some debug logging for duplicated nodes 2019-07-02 20:35:06 +02:00
zadam
d18a20cc06 fix focus issue from title to the text content with tab/enter 2019-07-02 20:28:57 +02:00
zadam
e94669de03 Merge branch 'stable' 2019-07-01 21:36:02 +02:00
zadam
9c91b0459e release 0.33.5 2019-06-30 21:47:04 +02:00
zadam
b161db064e choose only desired context menu items 2019-06-30 21:34:19 +02:00
zadam
ec4abe0d81 Merge branch 'stable' 2019-06-30 20:15:37 +02:00
zadam
af21dd4463 fix mobile text editor display 2019-06-30 20:14:57 +02:00
zadam
ef46727870 Merge branch 'stable' 2019-06-30 19:58:56 +02:00
zadam
1ea0d283de fix text instance sometimes remaining displayed when switching to e.g. image 2019-06-30 19:41:26 +02:00
zadam
ed380e09c9 context menu WIP 2019-06-30 18:56:46 +02:00
zadam
b5daa83d69 upgrades + removal of unused test 2019-06-30 11:22:10 +02:00
zadam
c4b957427d implement print, closes #581 2019-06-29 22:57:47 +02:00
zadam
2f3b256272 Merge branch 'stable' 2019-06-29 20:37:51 +02:00
zadam
6e3d8472e1 avoid duplicate key error 2019-06-28 21:50:15 +02:00
zadam
2a9f36a027 fix activating parent note after delete 2019-06-27 22:58:04 +02:00
zadam
cf3726289c attempt to fix the duplicate issue 2019-06-27 21:24:25 +02:00
zadam
3851bedb57 Merge branch 't34' 2019-06-26 21:11:27 +02:00
zadam
174128447b token auth to /login 2019-06-23 21:22:08 +02:00
zadam
5d213eea7e basic webp support 2019-06-23 15:22:05 +02:00
zadam
f45e25172b opening links from the clipper 2019-06-23 13:25:00 +02:00
zadam
ec87856ef4 save image 2019-06-23 12:16:26 +02:00
zadam
6feb7ad1d5 selection clipping now supports images 2019-06-23 11:25:15 +02:00
zadam
6833e84d55 fix pngquant for linux-x64 2019-06-23 09:10:06 +02:00
zadam
154a575701 cleanup + context menu clip now works 2019-06-22 19:49:48 +02:00
50 changed files with 1907 additions and 806 deletions

5
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# Default ignored files
/workspace.xml
# Datasource local storage ignored files
/dataSources.local.xml

View File

@@ -1,4 +1,4 @@
FROM node:12.4.0-alpine
FROM node:12.6.0-alpine
# Create app directory
WORKDIR /usr/src/app

View File

@@ -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
* [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

View File

@@ -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

View File

@@ -1,5 +1,7 @@
#!/usr/bin/env bash
export GITHUB_REPO=trilium
if [[ $# -eq 0 ]] ; then
echo "Missing argument of new version"
exit 1

View File

@@ -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).

View File

@@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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;
}

335
libraries/printThis.js Executable file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.33.4",
"version": "0.34.0-beta",
"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,23 @@
"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.2",
"electron-debug": "3.0.0",
"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": "12.0.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",
"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 +52,37 @@
"jimp": "0.6.4",
"mime-types": "2.1.24",
"moment": "2.24.0",
"multer": "1.4.1",
"multer": "1.4.2",
"node-abi": "2.9.0",
"open": "6.3.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.9",
"tar-stream": "2.1.0",
"turndown": "5.0.3",
"unescape": "1.0.1",
"ws": "7.0.1",
"ws": "7.1.0",
"xml2js": "0.4.19"
},
"devDependencies": {
"devtron": "1.4.0",
"electron": "6.0.0-beta.11",
"electron-builder": "20.44.2",
"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": {

View File

@@ -147,6 +147,26 @@ $noteTabContainer.on("click", ".export-note-button", function () {
$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
});

View File

@@ -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) {

View File

@@ -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"));

View File

@@ -6,6 +6,7 @@ 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);
@@ -82,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;
}
@@ -102,7 +103,7 @@ 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();
}

View File

@@ -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
}

View File

@@ -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,13 +85,17 @@ setTimeout(() => {
console.log("Lost connection to server");
}
try {
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify({
type: 'ping',
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);
}, 0);

View File

@@ -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;
}
@@ -544,6 +560,7 @@ export default {
getTabContexts,
getActiveTabContext,
getActiveEditor,
activateOrOpenNote,
clearOpenTabsTask,
filterTabs,
openEmptyTab,

View File

@@ -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,

View File

@@ -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";

View File

@@ -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() {

View File

@@ -83,7 +83,11 @@ class TabContext {
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");
@@ -92,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) {
@@ -127,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() {
@@ -338,7 +342,9 @@ class TabContext {
}
closeAutocomplete() {
this.$tabContent.find('.aa-input').autocomplete('close');
if (utils.isDesktop()) {
this.$tabContent.find('.aa-input').autocomplete('close');
}
}
}

View File

@@ -196,6 +196,8 @@ async function resolveNotePath(notePath) {
async function getRunPath(notePath) {
utils.assertArguments(notePath);
notePath = notePath.split("-")[0];
const path = notePath.split("/").reverse();
if (!path.includes("root")) {
@@ -335,6 +337,31 @@ async function treeInitialized() {
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 = [];
for (const openTab of openTabs) {
@@ -630,7 +657,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') {
@@ -638,10 +666,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);
@@ -707,6 +739,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 => {
@@ -737,10 +778,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) {

View File

@@ -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') {

View File

@@ -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) {

View File

@@ -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 {
@@ -354,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;
}
@@ -832,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 */
}

132
src/routes/api/clipper.js Normal file
View 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
};

View File

@@ -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];
}

View File

@@ -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
};

View File

@@ -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
};

View File

@@ -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);
}

View File

@@ -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
};

View File

@@ -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
};

View File

@@ -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" };

View File

@@ -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();

5
src/services/env.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
isDev: function () {
return process.env.TRILIUM_ENV && process.env.TRILIUM_ENV === 'dev';
}
};

View File

@@ -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);

10
src/services/host.js Normal file
View 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');

View File

@@ -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,

View File

@@ -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);

View File

@@ -75,8 +75,6 @@ function getMime(fileName) {
const ext = path.extname(fileName).toLowerCase();
if (ext in EXTENSION_TO_MIME) {
console.log(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);
}
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);
}
@@ -108,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();

View File

@@ -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');
}

View File

@@ -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();
});

View File

@@ -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>

View File

@@ -49,7 +49,9 @@
</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>

View File

@@ -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 %>

View File

@@ -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>

View File

@@ -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;
}
}
}