mirror of
https://github.com/zadam/trilium.git
synced 2025-11-01 10:55:55 +01:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b32addade | ||
|
|
0b251530fa | ||
|
|
f5b933149a | ||
|
|
48bbfb8bdb | ||
|
|
889971c4d6 | ||
|
|
0722494d41 | ||
|
|
4b977a3306 | ||
|
|
3ff3021acd | ||
|
|
99e56a9c42 | ||
|
|
77279dfe16 | ||
|
|
93f8050454 | ||
|
|
31cfede7a7 | ||
|
|
c8ec86e537 | ||
|
|
05aee884b6 | ||
|
|
012ba9e060 | ||
|
|
8e8fd88857 | ||
|
|
523ccdad6b | ||
|
|
ded3f605be | ||
|
|
030d12a465 | ||
|
|
4d15628840 | ||
|
|
81b849898c | ||
|
|
3824486b85 | ||
|
|
081ab00a0a | ||
|
|
04f6af5c9a | ||
|
|
4dc1f1f6eb | ||
|
|
3930a02123 | ||
|
|
3112de105e |
7
.gitpod.yml
Normal file
7
.gitpod.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
tasks:
|
||||||
|
- before: nvm install 10 && nvm use 10
|
||||||
|
init: npm install
|
||||||
|
command: npm run start
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
||||||
|
onOpen: open-preview
|
||||||
@@ -17,6 +17,7 @@ RUN set -x \
|
|||||||
libtool \
|
libtool \
|
||||||
make \
|
make \
|
||||||
nasm \
|
nasm \
|
||||||
|
libpng-dev \
|
||||||
&& npm install --production \
|
&& npm install --production \
|
||||||
&& apk del .build-dependencies
|
&& apk del .build-dependencies
|
||||||
|
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -35,3 +35,15 @@ Trilium is provided as either desktop application (Linux, Windows, Mac) or web a
|
|||||||
[See wiki for complete list of documentation pages.](https://github.com/zadam/trilium/wiki/)
|
[See wiki for complete list of documentation pages.](https://github.com/zadam/trilium/wiki/)
|
||||||
|
|
||||||
You can also read [Patterns of personal knowledge base](https://github.com/zadam/trilium/wiki/Patterns-of-personal-knowledge-base) to get some inspiration on how you might use Trilium.
|
You can also read [Patterns of personal knowledge base](https://github.com/zadam/trilium/wiki/Patterns-of-personal-knowledge-base) to get some inspiration on how you might use Trilium.
|
||||||
|
|
||||||
|
## Contribute
|
||||||
|
|
||||||
|
Use a browser based dev environment
|
||||||
|
|
||||||
|
[](https://gitpod.io/#https://github.com/zadam/trilium)
|
||||||
|
|
||||||
|
Or clone locally and run
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "trilium",
|
"name": "trilium",
|
||||||
"productName": "Trilium Notes",
|
"productName": "Trilium Notes",
|
||||||
"description": "Trilium Notes",
|
"description": "Trilium Notes",
|
||||||
"version": "0.27.3",
|
"version": "0.27.4",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"main": "electron.js",
|
"main": "electron.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
"electron-in-page-search": "1.3.2",
|
"electron-in-page-search": "1.3.2",
|
||||||
"express": "4.16.4",
|
"express": "4.16.4",
|
||||||
"express-session": "1.15.6",
|
"express-session": "1.15.6",
|
||||||
|
"file-type": "10.7.0",
|
||||||
"fs-extra": "7.0.1",
|
"fs-extra": "7.0.1",
|
||||||
"get-port": "4.1.0",
|
"get-port": "4.1.0",
|
||||||
"helmet": "3.15.0",
|
"helmet": "3.15.0",
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ class Note extends Entity {
|
|||||||
setContent(content) {
|
setContent(content) {
|
||||||
this.content = content;
|
this.content = content;
|
||||||
|
|
||||||
|
// if parsing below is not successful then there's no jsonContent as opposed to still having the old unupdated ones
|
||||||
|
delete this.jsonContent;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.jsonContent = JSON.parse(this.content);
|
this.jsonContent = JSON.parse(this.content);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import hoistedNoteService from './services/hoisted_note.js';
|
|||||||
import noteTypeService from './services/note_type.js';
|
import noteTypeService from './services/note_type.js';
|
||||||
import linkService from './services/link.js';
|
import linkService from './services/link.js';
|
||||||
import noteAutocompleteService from './services/note_autocomplete.js';
|
import noteAutocompleteService from './services/note_autocomplete.js';
|
||||||
|
import macInit from './services/mac_init.js';
|
||||||
|
|
||||||
// required for CKEditor image upload plugin
|
// required for CKEditor image upload plugin
|
||||||
window.glob.getCurrentNode = treeService.getCurrentNode;
|
window.glob.getCurrentNode = treeService.getCurrentNode;
|
||||||
@@ -110,28 +111,6 @@ if (utils.isElectron()) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function exec(cmd) {
|
|
||||||
document.execCommand(cmd);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utils.isElectron() && utils.isMac()) {
|
|
||||||
utils.bindShortcut('ctrl+c', () => exec("copy"));
|
|
||||||
utils.bindShortcut('ctrl+v', () => exec('paste'));
|
|
||||||
utils.bindShortcut('ctrl+x', () => exec('cut'));
|
|
||||||
utils.bindShortcut('ctrl+a', () => exec('selectAll'));
|
|
||||||
utils.bindShortcut('ctrl+z', () => exec('undo'));
|
|
||||||
utils.bindShortcut('ctrl+y', () => exec('redo'));
|
|
||||||
|
|
||||||
utils.bindShortcut('meta+c', () => exec("copy"));
|
|
||||||
utils.bindShortcut('meta+v', () => exec('paste'));
|
|
||||||
utils.bindShortcut('meta+x', () => exec('cut'));
|
|
||||||
utils.bindShortcut('meta+a', () => exec('selectAll'));
|
|
||||||
utils.bindShortcut('meta+z', () => exec('undo'));
|
|
||||||
utils.bindShortcut('meta+y', () => exec('redo'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#export-note-button").click(function () {
|
$("#export-note-button").click(function () {
|
||||||
if ($(this).hasClass("disabled")) {
|
if ($(this).hasClass("disabled")) {
|
||||||
return;
|
return;
|
||||||
@@ -140,6 +119,8 @@ $("#export-note-button").click(function () {
|
|||||||
exportDialog.showDialog('single');
|
exportDialog.showDialog('single');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
macInit.init();
|
||||||
|
|
||||||
treeService.showTree();
|
treeService.showTree();
|
||||||
|
|
||||||
entrypoints.registerEntrypoints();
|
entrypoints.registerEntrypoints();
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ const $dialog = $("#jump-to-note-dialog");
|
|||||||
const $autoComplete = $("#jump-to-note-autocomplete");
|
const $autoComplete = $("#jump-to-note-autocomplete");
|
||||||
const $showInFullTextButton = $("#show-in-full-text-button");
|
const $showInFullTextButton = $("#show-in-full-text-button");
|
||||||
|
|
||||||
$dialog.on("shown.bs.modal", e => $autoComplete.focus());
|
|
||||||
|
|
||||||
async function showDialog() {
|
async function showDialog() {
|
||||||
glob.activeDialog = $dialog;
|
glob.activeDialog = $dialog;
|
||||||
|
|
||||||
|
|||||||
@@ -117,10 +117,6 @@ function registerEntrypoints() {
|
|||||||
|
|
||||||
utils.bindShortcut('ctrl+f', openInPageSearch);
|
utils.bindShortcut('ctrl+f', openInPageSearch);
|
||||||
|
|
||||||
if (utils.isMac()) {
|
|
||||||
utils.bindShortcut('meta+f', openInPageSearch);
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: do we really need these at this point?
|
// FIXME: do we really need these at this point?
|
||||||
utils.bindShortcut("ctrl+shift+up", () => {
|
utils.bindShortcut("ctrl+shift+up", () => {
|
||||||
const node = treeService.getCurrentNode();
|
const node = treeService.getCurrentNode();
|
||||||
|
|||||||
25
src/public/javascripts/services/mac_init.js
Normal file
25
src/public/javascripts/services/mac_init.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Mac specific initialization
|
||||||
|
*/
|
||||||
|
import utils from "./utils.js";
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (utils.isElectron() && utils.isMac()) {
|
||||||
|
utils.bindShortcut('meta+c', () => exec("copy"));
|
||||||
|
utils.bindShortcut('meta+v', () => exec('paste'));
|
||||||
|
utils.bindShortcut('meta+x', () => exec('cut'));
|
||||||
|
utils.bindShortcut('meta+a', () => exec('selectAll'));
|
||||||
|
utils.bindShortcut('meta+z', () => exec('undo'));
|
||||||
|
utils.bindShortcut('meta+y', () => exec('redo'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exec(cmd) {
|
||||||
|
document.execCommand(cmd);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
init
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ function clearText($el) {
|
|||||||
function showRecentNotes($el) {
|
function showRecentNotes($el) {
|
||||||
$el.setSelectedPath("");
|
$el.setSelectedPath("");
|
||||||
$el.autocomplete("val", "");
|
$el.autocomplete("val", "");
|
||||||
$el.autocomplete("open");
|
$el.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initNoteAutocomplete($el, options) {
|
function initNoteAutocomplete($el, options) {
|
||||||
@@ -61,7 +61,13 @@ function initNoteAutocomplete($el, options) {
|
|||||||
|
|
||||||
$clearTextButton.click(() => clearText($el));
|
$clearTextButton.click(() => clearText($el));
|
||||||
|
|
||||||
$showRecentNotesButton.click(() => showRecentNotes($el));
|
$showRecentNotesButton.click(e => {
|
||||||
|
showRecentNotes($el);
|
||||||
|
|
||||||
|
// this will cause the click not give focus to the "show recent notes" button
|
||||||
|
// this is important because otherwise input will lose focus immediatelly and not show the results
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
$goToSelectedNoteButton.click(() => {
|
$goToSelectedNoteButton.click(() => {
|
||||||
if ($el.hasClass("disabled")) {
|
if ($el.hasClass("disabled")) {
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import noteDetailService from "./note_detail.js";
|
import noteDetailService from "./note_detail.js";
|
||||||
|
import treeService from "./tree.js";
|
||||||
|
import infoService from './info.js';
|
||||||
|
|
||||||
const $searchString = $("#search-string");
|
const $searchString = $("#search-string");
|
||||||
const $component = $('#note-detail-search');
|
const $component = $('#note-detail-search');
|
||||||
|
const $refreshButton = $('#note-detail-search-refresh-results-button');
|
||||||
|
|
||||||
function getContent() {
|
function getContent() {
|
||||||
JSON.stringify({
|
return JSON.stringify({
|
||||||
searchString: $searchString.val()
|
searchString: $searchString.val()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -25,6 +28,14 @@ function show() {
|
|||||||
$searchString.on('input', noteDetailService.noteChanged);
|
$searchString.on('input', noteDetailService.noteChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$refreshButton.click(async () => {
|
||||||
|
await noteDetailService.saveNoteIfChanged();
|
||||||
|
|
||||||
|
treeService.reload();
|
||||||
|
|
||||||
|
infoService.showMessage('Tree has been refreshed.');
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getContent,
|
getContent,
|
||||||
show,
|
show,
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ $searchInput.keyup(e => {
|
|||||||
if (e && e.which === $.ui.keyCode.ENTER) {
|
if (e && e.which === $.ui.keyCode.ENTER) {
|
||||||
doSearch();
|
doSearch();
|
||||||
}
|
}
|
||||||
}).focus();
|
});
|
||||||
|
|
||||||
$doSearchButton.click(() => doSearch()); // keep long form because of argument
|
$doSearchButton.click(() => doSearch()); // keep long form because of argument
|
||||||
$resetSearchButton.click(resetSearch);
|
$resetSearchButton.click(resetSearch);
|
||||||
|
|||||||
@@ -94,9 +94,18 @@ async function expandToNote(notePath, expandOpts) {
|
|||||||
|
|
||||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||||
|
|
||||||
|
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
|
||||||
|
let hoistedNoteFound = false;
|
||||||
|
|
||||||
let parentNoteId = null;
|
let parentNoteId = null;
|
||||||
|
|
||||||
for (const childNoteId of runPath) {
|
for (const childNoteId of runPath) {
|
||||||
|
if (childNoteId === hoistedNoteId) {
|
||||||
|
hoistedNoteFound = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we expand only after hoisted note since before then nodes are not actually present in the tree
|
||||||
|
if (hoistedNoteFound) {
|
||||||
// for first node (!parentNoteId) it doesn't matter which node is found
|
// for first node (!parentNoteId) it doesn't matter which node is found
|
||||||
let node = getNode(childNoteId, parentNoteId);
|
let node = getNode(childNoteId, parentNoteId);
|
||||||
|
|
||||||
@@ -117,10 +126,10 @@ async function expandToNote(notePath, expandOpts) {
|
|||||||
|
|
||||||
if (childNoteId === noteId) {
|
if (childNoteId === noteId) {
|
||||||
return node;
|
return node;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
await node.setExpanded(true, expandOpts);
|
await node.setExpanded(true, expandOpts);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
parentNoteId = childNoteId;
|
parentNoteId = childNoteId;
|
||||||
}
|
}
|
||||||
@@ -129,9 +138,12 @@ async function expandToNote(notePath, expandOpts) {
|
|||||||
async function activateNote(notePath, noteLoadedListener) {
|
async function activateNote(notePath, noteLoadedListener) {
|
||||||
utils.assertArguments(notePath);
|
utils.assertArguments(notePath);
|
||||||
|
|
||||||
|
// notePath argument can contain only noteId which is not good when hoisted since
|
||||||
|
// then we need to check the whole note path
|
||||||
|
const runNotePath = await getRunPath(notePath);
|
||||||
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
|
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
|
||||||
|
|
||||||
if (hoistedNoteId !== 'root' && !notePath.includes(hoistedNoteId)) {
|
if (hoistedNoteId !== 'root' && !runNotePath.includes(hoistedNoteId)) {
|
||||||
if (!await confirmDialog.confirm("Requested note is outside of hoisted note subtree. Do you want to unhoist?")) {
|
if (!await confirmDialog.confirm("Requested note is outside of hoisted note subtree. Do you want to unhoist?")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -352,6 +364,7 @@ function clearSelectedNodes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function treeInitialized() {
|
async function treeInitialized() {
|
||||||
|
// - is used in mobile to indicate that we don't want to activate any note after load
|
||||||
if (startNotePath === '-') {
|
if (startNotePath === '-') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -363,7 +376,6 @@ async function treeInitialized() {
|
|||||||
startNotePath = null;
|
startNotePath = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// - is used in mobile to indicate that we don't want to activate any note after load
|
|
||||||
if (startNotePath) {
|
if (startNotePath) {
|
||||||
const node = await activateNote(startNotePath);
|
const node = await activateNote(startNotePath);
|
||||||
|
|
||||||
@@ -438,6 +450,16 @@ function initFancyTree(tree) {
|
|||||||
|
|
||||||
$span.append(unhoistButton);
|
$span.append(unhoistButton);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
// this is done to automatically lazy load all expanded search notes after tree load
|
||||||
|
loadChildren: function(event, data) {
|
||||||
|
data.node.visit(function(subNode){
|
||||||
|
// Load all lazy/unloaded child nodes
|
||||||
|
// (which will trigger `loadChildren` recursively)
|
||||||
|
if( subNode.isUndefined() && subNode.isExpanded() ) {
|
||||||
|
subNode.load();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ async function prepareNode(branch) {
|
|||||||
extraClasses: await getExtraClasses(note),
|
extraClasses: await getExtraClasses(note),
|
||||||
icon: await getIcon(note),
|
icon: await getIcon(note),
|
||||||
refKey: note.noteId,
|
refKey: note.noteId,
|
||||||
expanded: (note.type !== 'search' && branch.isExpanded) || hoistedNoteId === note.noteId
|
expanded: branch.isExpanded || hoistedNoteId === note.noteId
|
||||||
};
|
};
|
||||||
|
|
||||||
if (note.hasChildren() || note.type === 'search') {
|
if (note.hasChildren() || note.type === 'search') {
|
||||||
|
|||||||
@@ -137,6 +137,11 @@ function randomString(len) {
|
|||||||
|
|
||||||
function bindShortcut(keyboardShortcut, handler) {
|
function bindShortcut(keyboardShortcut, handler) {
|
||||||
if (isDesktop()) {
|
if (isDesktop()) {
|
||||||
|
if (isMac()) {
|
||||||
|
// use CMD (meta) instead of CTRL for all shortcuts
|
||||||
|
keyboardShortcut = keyboardShortcut.replace("ctrl", "meta");
|
||||||
|
}
|
||||||
|
|
||||||
$(document).bind('keydown', keyboardShortcut, e => {
|
$(document).bind('keydown', keyboardShortcut, e => {
|
||||||
handler();
|
handler();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import utils from "./services/utils.js";
|
import utils from "./services/utils.js";
|
||||||
|
import macInit from './services/mac_init.js';
|
||||||
|
|
||||||
|
macInit.init();
|
||||||
|
|
||||||
function SetupModel() {
|
function SetupModel() {
|
||||||
if (syncInProgress) {
|
if (syncInProgress) {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
padding: 10px 0 10px 0;
|
padding: 10px 0 10px 0;
|
||||||
margin: 0 20px 0 10px;
|
margin: 0 20px 0 10px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 5px;
|
border-radius: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#context-menu-container {
|
#context-menu-container {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ body {
|
|||||||
/* Fix for CKEditor block gutter icon "stretching" body and causing scrollbar to appear after pressing enter
|
/* Fix for CKEditor block gutter icon "stretching" body and causing scrollbar to appear after pressing enter
|
||||||
on the last line of the editor. */
|
on the last line of the editor. */
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#title-container {
|
#title-container {
|
||||||
@@ -16,6 +17,16 @@ body {
|
|||||||
flex-grow: 100;
|
flex-grow: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.fancytree-container {
|
||||||
|
/* override specific size from fancytree.css */
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancytree-title {
|
||||||
|
margin-left: 7px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.fancytree-node:not(.fancytree-loading) .fancytree-expander {
|
.fancytree-node:not(.fancytree-loading) .fancytree-expander {
|
||||||
background: none;
|
background: none;
|
||||||
width: auto;
|
width: auto;
|
||||||
@@ -131,8 +142,21 @@ span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-tit
|
|||||||
|
|
||||||
/* By default not focused active tree item is not easily visible, this makes it more visible */
|
/* By default not focused active tree item is not easily visible, this makes it more visible */
|
||||||
span.fancytree-active:not(.fancytree-focused) .fancytree-title {
|
span.fancytree-active:not(.fancytree-focused) .fancytree-title {
|
||||||
|
background-color: #eee !important;
|
||||||
|
border-color: #ddd !important;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.fancytree-active.fancytree-focused .fancytree-title {
|
||||||
background-color: #ddd !important;
|
background-color: #ddd !important;
|
||||||
border-color: #555 !important;
|
border-color: #bbb !important;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancytree-plain span.fancytree-node:hover span.fancytree-title {
|
||||||
|
background-color: #eee !important;
|
||||||
|
border-color: #bbb !important;
|
||||||
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-autocomplete {
|
.ui-autocomplete {
|
||||||
@@ -366,7 +390,7 @@ div.ui-tooltip {
|
|||||||
height: 150px;
|
height: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:not(.btn-primary):not(.btn-danger) {
|
.btn:not(.btn-primary):not(.btn-secondary):not(.btn-danger) {
|
||||||
border-color: #ddd;
|
border-color: #ddd;
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const tarImportService = require('../../services/import/tar');
|
|||||||
const singleImportService = require('../../services/import/single');
|
const singleImportService = require('../../services/import/single');
|
||||||
const cls = require('../../services/cls');
|
const cls = require('../../services/cls');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const noteCacheService = require('../../services/note_cache');
|
||||||
|
|
||||||
async function importToBranch(req) {
|
async function importToBranch(req) {
|
||||||
const parentNoteId = req.params.parentNoteId;
|
const parentNoteId = req.params.parentNoteId;
|
||||||
@@ -28,24 +29,32 @@ async function importToBranch(req) {
|
|||||||
// and may produce unintended consequences
|
// and may produce unintended consequences
|
||||||
cls.disableEntityEvents();
|
cls.disableEntityEvents();
|
||||||
|
|
||||||
|
let note; // typically root of the import - client can show it after finishing the import
|
||||||
|
|
||||||
if (extension === '.tar') {
|
if (extension === '.tar') {
|
||||||
return await tarImportService.importTar(file.buffer, parentNote);
|
note = await tarImportService.importTar(file.buffer, parentNote);
|
||||||
}
|
}
|
||||||
else if (extension === '.opml') {
|
else if (extension === '.opml') {
|
||||||
return await opmlImportService.importOpml(file.buffer, parentNote);
|
note = await opmlImportService.importOpml(file.buffer, parentNote);
|
||||||
}
|
}
|
||||||
else if (extension === '.md') {
|
else if (extension === '.md') {
|
||||||
return await singleImportService.importMarkdown(file, parentNote);
|
note = await singleImportService.importMarkdown(file, parentNote);
|
||||||
}
|
}
|
||||||
else if (extension === '.html' || extension === '.htm') {
|
else if (extension === '.html' || extension === '.htm') {
|
||||||
return await singleImportService.importHtml(file, parentNote);
|
note = await singleImportService.importHtml(file, parentNote);
|
||||||
}
|
}
|
||||||
else if (extension === '.enex') {
|
else if (extension === '.enex') {
|
||||||
return await enexImportService.importEnex(file, parentNote);
|
note = await enexImportService.importEnex(file, parentNote);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return [400, `Unrecognized extension ${extension}, must be .tar or .opml`];
|
return [400, `Unrecognized extension ${extension}, must be .tar or .opml`];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// import has deactivated note events so note cache is not updated
|
||||||
|
// instead we force it to reload (can be async)
|
||||||
|
noteCacheService.load();
|
||||||
|
|
||||||
|
return note;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ async function loginSync(req) {
|
|||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
if (Math.abs(timestamp.getTime() - now.getTime()) > 5000) {
|
// login token is valid for 5 minutes
|
||||||
|
if (Math.abs(timestamp.getTime() - now.getTime()) > 5 * 60 * 1000) {
|
||||||
return [400, { message: 'Auth request time is out of sync' }];
|
return [400, { message: 'Auth request time is out of sync' }];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ async function anonymize() {
|
|||||||
await db.run("UPDATE notes SET title = 'title', content = 'text'");
|
await db.run("UPDATE notes SET title = 'title', content = 'text'");
|
||||||
await db.run("UPDATE note_revisions SET title = 'title', content = 'text'");
|
await db.run("UPDATE note_revisions SET title = 'title', content = 'text'");
|
||||||
await db.run("UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL");
|
await db.run("UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL");
|
||||||
await db.run("UPDATE images SET data = NULL");
|
|
||||||
await db.run(`UPDATE options SET value = 'anonymized' WHERE name IN
|
await db.run(`UPDATE options SET value = 'anonymized' WHERE name IN
|
||||||
('documentSecret', 'encryptedDataKey', 'passwordVerificationHash',
|
('documentSecret', 'encryptedDataKey', 'passwordVerificationHash',
|
||||||
'passwordVerificationSalt', 'passwordDerivedKeySalt')`);
|
'passwordVerificationSalt', 'passwordDerivedKeySalt')`);
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
module.exports = { buildDate:"2019-01-05T22:48:11+01:00", buildRevision: "9fca7f09a564c719c834d38d76c3b595c8579b2a" };
|
module.exports = { buildDate:"2019-01-10T21:31:30+01:00", buildRevision: "0b251530fa0ee61edc8dcc9235033abb73afc614" };
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const repository = require('./repository');
|
const repository = require('./repository');
|
||||||
|
const log = require('./log');
|
||||||
const protectedSessionService = require('./protected_session');
|
const protectedSessionService = require('./protected_session');
|
||||||
const noteService = require('./notes');
|
const noteService = require('./notes');
|
||||||
const imagemin = require('imagemin');
|
const imagemin = require('imagemin');
|
||||||
@@ -13,7 +14,13 @@ const sanitizeFilename = require('sanitize-filename');
|
|||||||
|
|
||||||
async function saveImage(buffer, originalName, parentNoteId) {
|
async function saveImage(buffer, originalName, parentNoteId) {
|
||||||
const resizedImage = await resize(buffer);
|
const resizedImage = await resize(buffer);
|
||||||
const optimizedImage = await optimize(resizedImage);
|
let optimizedImage;
|
||||||
|
try {
|
||||||
|
optimizedImage = await optimize(resizedImage);
|
||||||
|
} catch (e) {
|
||||||
|
log.error(e);
|
||||||
|
optimizedImage = resizedImage;
|
||||||
|
}
|
||||||
|
|
||||||
const imageFormat = imageType(optimizedImage);
|
const imageFormat = imageType(optimizedImage);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const sax = require("sax");
|
const sax = require("sax");
|
||||||
|
const fileType = require('file-type');
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
const xml2js = require('xml2js');
|
const xml2js = require('xml2js');
|
||||||
const log = require("../log");
|
const log = require("../log");
|
||||||
@@ -144,7 +145,7 @@ async function importEnex(file, parentNote) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (currentTag === 'mime') {
|
else if (currentTag === 'mime') {
|
||||||
resource.mime = text;
|
resource.mime = text.toLowerCase();
|
||||||
|
|
||||||
if (text.startsWith("image/")) {
|
if (text.startsWith("image/")) {
|
||||||
resource.title = "image";
|
resource.title = "image";
|
||||||
@@ -222,7 +223,26 @@ async function importEnex(file, parentNote) {
|
|||||||
|
|
||||||
const mediaRegex = new RegExp(`<en-media hash="${hash}"[^>]*>`, 'g');
|
const mediaRegex = new RegExp(`<en-media hash="${hash}"[^>]*>`, 'g');
|
||||||
|
|
||||||
if (resource.mime.startsWith("image/")) {
|
const fileTypeFromBuffer = fileType(resource.content);
|
||||||
|
if (fileTypeFromBuffer) {
|
||||||
|
// If fileType returns something for buffer, then set the mime given
|
||||||
|
resource.mime = fileTypeFromBuffer.mime;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createResourceNote = async () => {
|
||||||
|
const resourceNote = (await noteService.createNote(noteEntity.noteId, resource.title, resource.content, {
|
||||||
|
attributes: resource.attributes,
|
||||||
|
type: 'file',
|
||||||
|
mime: resource.mime
|
||||||
|
})).note;
|
||||||
|
|
||||||
|
const resourceLink = `<a href="#root/${resourceNote.noteId}">${utils.escapeHtml(resource.title)}</a>`;
|
||||||
|
|
||||||
|
noteEntity.content = noteEntity.content.replace(mediaRegex, resourceLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (["image/jpeg", "image/png", "image/gif"].includes(resource.mime)) {
|
||||||
|
try {
|
||||||
const originalName = "image." + resource.mime.substr(6);
|
const originalName = "image." + resource.mime.substr(6);
|
||||||
|
|
||||||
const { url } = await imageService.saveImage(resource.content, originalName, noteEntity.noteId);
|
const { url } = await imageService.saveImage(resource.content, originalName, noteEntity.noteId);
|
||||||
@@ -236,17 +256,13 @@ async function importEnex(file, parentNote) {
|
|||||||
// otherwise image would be removed since no note would include it
|
// otherwise image would be removed since no note would include it
|
||||||
note.content += imageLink;
|
note.content += imageLink;
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.error("error when saving image from ENEX file: " + e);
|
||||||
|
await createResourceNote();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const resourceNote = (await noteService.createNote(noteEntity.noteId, resource.title, resource.content, {
|
await createResourceNote();
|
||||||
attributes: resource.attributes,
|
|
||||||
type: 'file',
|
|
||||||
mime: resource.mime
|
|
||||||
})).note;
|
|
||||||
|
|
||||||
const resourceLink = `<a href="#root/${resourceNote.noteId}">${utils.escapeHtml(resource.title)}</a>`;
|
|
||||||
|
|
||||||
noteEntity.content = noteEntity.content.replace(mediaRegex, resourceLink);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,21 @@ async function load() {
|
|||||||
|
|
||||||
archived = await sql.getMap(`SELECT noteId, isInheritable FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name = 'archived'`);
|
archived = await sql.getMap(`SELECT noteId, isInheritable FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name = 'archived'`);
|
||||||
|
|
||||||
|
if (protectedSessionService.isProtectedSessionAvailable()) {
|
||||||
|
await loadProtectedNotes();
|
||||||
|
}
|
||||||
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadProtectedNotes() {
|
||||||
|
protectedNoteTitles = await sql.getMap(`SELECT noteId, title FROM notes WHERE isDeleted = 0 AND isProtected = 1`);
|
||||||
|
|
||||||
|
for (const noteId in protectedNoteTitles) {
|
||||||
|
protectedNoteTitles[noteId] = protectedSessionService.decryptNoteTitle(noteId, protectedNoteTitles[noteId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function highlightResults(results, allTokens) {
|
function highlightResults(results, allTokens) {
|
||||||
// we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks
|
// we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks
|
||||||
// which would make the resulting HTML string invalid.
|
// which would make the resulting HTML string invalid.
|
||||||
@@ -313,8 +325,17 @@ eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED
|
|||||||
delete noteTitles[note.noteId];
|
delete noteTitles[note.noteId];
|
||||||
delete childToParent[note.noteId];
|
delete childToParent[note.noteId];
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
if (note.isProtected) {
|
||||||
|
// we can assume we have protected session since we managed to update
|
||||||
|
// removing from the maps is important when switching between protected & unprotected
|
||||||
|
protectedNoteTitles[note.noteId] = note.title;
|
||||||
|
delete noteTitles[note.noteId];
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
noteTitles[note.noteId] = note.title;
|
noteTitles[note.noteId] = note.title;
|
||||||
|
delete protectedNoteTitles[note.noteId];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (entityName === 'branches') {
|
else if (entityName === 'branches') {
|
||||||
@@ -355,15 +376,9 @@ eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, async () => {
|
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
|
||||||
if (!loaded) {
|
if (loaded) {
|
||||||
return;
|
loadProtectedNotes();
|
||||||
}
|
|
||||||
|
|
||||||
protectedNoteTitles = await sql.getMap(`SELECT noteId, title FROM notes WHERE isDeleted = 0 AND isProtected = 1`);
|
|
||||||
|
|
||||||
for (const noteId in protectedNoteTitles) {
|
|
||||||
protectedNoteTitles[noteId] = protectedSessionService.decryptNoteTitle(noteId, protectedNoteTitles[noteId]);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -372,5 +387,6 @@ sqlInit.dbReady.then(() => utils.stopWatch("Autocomplete load", load));
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
findNotes,
|
findNotes,
|
||||||
getNotePath,
|
getNotePath,
|
||||||
getNoteTitleForPath
|
getNoteTitleForPath,
|
||||||
|
load
|
||||||
};
|
};
|
||||||
@@ -6,7 +6,7 @@ const sourceIdService = require('./source_id');
|
|||||||
const log = require('./log');
|
const log = require('./log');
|
||||||
|
|
||||||
async function executeNote(note, originEntity) {
|
async function executeNote(note, originEntity) {
|
||||||
if (!note.isJavaScript()) {
|
if (!note.isJavaScript() || !note.isContentAvailable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +80,10 @@ function getParams(params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = []) {
|
async function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = []) {
|
||||||
|
if (!note.isContentAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!note.isJavaScript() && !note.isHtml()) {
|
if (!note.isJavaScript() && !note.isHtml()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
<div id="note-detail-search" class="note-detail-component">
|
<div id="note-detail-search" class="note-detail-component">
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center;">
|
||||||
<strong>Search string: </strong>
|
<strong>Search string: </strong>
|
||||||
<textarea rows="4" cols="50" id="search-string"></textarea>
|
<textarea rows="4" cols="40" id="search-string"></textarea>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-primary" id="note-detail-search-refresh-results-button">Refresh tree</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
Reference in New Issue
Block a user