Compare commits

...

60 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
c8e4a5c9e7 release 0.33.4 2019-06-26 21:20:30 +02:00
zadam
3851bedb57 Merge branch 't34' 2019-06-26 21:11:27 +02:00
zadam
a3951f1cce make sure autocomplete is closed when navigating to note 2019-06-26 21:08:54 +02:00
zadam
7c77ae758b fix frontend reload again 2019-06-26 20:49:17 +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 1919 additions and 809 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
@@ -74,7 +95,7 @@ async function createMainWindow() {
const parsedUrl = url.parse(targetUrl);
// we still need to allow internal redirects from setup and migration pages
if (parsedUrl.hostname !== 'localhost' || parsedUrl.hostname !== '127.0.0.1' || (parsedUrl.path && parsedUrl.path !== '/')) {
if (!['localhost', '127.0.0.1'].includes(parsedUrl.hostname) || (parsedUrl.path && parsedUrl.path !== '/')) {
ev.preventDefault();
}
});

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.3",
"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.10",
"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,10 +85,17 @@ setTimeout(() => {
console.log("Lost connection to server");
}
ws.send(JSON.stringify({
type: 'ping',
lastSyncId: lastSyncId
}));
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify({
type: 'ping',
lastSyncId: lastSyncId
}));
}
else if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
console.log("WS closed or closing, trying to reconnect");
ws = connectWebSocket();
}
}, 1000);
}, 0);

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;
}
@@ -405,7 +421,7 @@ tabRow.addListener('tabRemove', async ({ detail }) => {
if (tabContextToDelete) {
// sometimes there are orphan autocompletes after closing the tab
tabContextToDelete.$tabContent.find('.aa-input').autocomplete('close');
tabContextToDelete.closeAutocomplete();
await tabContextToDelete.saveNoteIfChanged();
tabContextToDelete.$tabContent.remove();
@@ -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) {
@@ -109,6 +113,8 @@ class TabContext {
this.setTitleBar();
this.closeAutocomplete(); // esp. on windows autocomplete is not getting closed automatically
setTimeout(async () => {
// we include the note into recent list only if the user stayed on the note at least 5 seconds
if (notePath && notePath === this.notePath) {
@@ -125,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() {
@@ -334,6 +340,12 @@ class TabContext {
}
}
}
closeAutocomplete() {
if (utils.isDesktop()) {
this.$tabContent.find('.aa-input').autocomplete('close');
}
}
}
export default TabContext;

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-24T20:47:50+02:00", buildRevision: "1b831f94a98dbbb7184048f329ea89175841dec2" };
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;
}
}
}