Compare commits

...

57 Commits

Author SHA1 Message Date
azivner
2b32addade release 0.27.4 2019-01-10 21:31:30 +01:00
azivner
0b251530fa open recent notes autocomplete by focus so it is then closed with blur, fixes #272 2019-01-10 21:04:06 +01:00
azivner
f5b933149a Merge remote-tracking branch 'origin/master' 2019-01-10 19:53:47 +01:00
azivner
48bbfb8bdb fix activating note by noteId when hoisting, fixes #320 2019-01-10 19:53:42 +01:00
zadam
889971c4d6 Merge pull request #312 from perissology/evernote-import
Fixes evernote import errors
2019-01-09 23:41:17 +01:00
azivner
0722494d41 fix saving JSON note with invalid JSON (previously in such a case content was not updated), fixes #307 2019-01-09 23:36:17 +01:00
azivner
4b977a3306 setup keyboard shortcuts on the setup page as well, closes #267 2019-01-09 22:08:24 +01:00
azivner
3ff3021acd shortcuts for mac should use cmd instead of ctrl, closes #290 2019-01-09 21:42:16 +01:00
azivner
99e56a9c42 make sure to save the search note before refreshing the tree 2019-01-09 19:54:32 +01:00
azivner
77279dfe16 fix anonymization 2019-01-09 19:49:02 +01:00
perissology
93f8050454 use resizedImage if image optimization fails 2019-01-09 06:29:49 -08:00
perissology
31cfede7a7 enex import: attempt to get correct mime from Buffer 2019-01-09 06:29:13 -08:00
azivner
c8ec86e537 allow refreshing saved note, closes #304 2019-01-08 23:32:03 +01:00
azivner
05aee884b6 fix saving search note content #304 2019-01-08 22:48:53 +01:00
azivner
012ba9e060 don't attempt to run protected notes outside of protected session, fixes #279 2019-01-08 21:21:49 +01:00
azivner
8e8fd88857 process only whitelisted mime types as an image, fixes #288 2019-01-08 20:45:34 +01:00
azivner
523ccdad6b reload note cache after import, closes #293 2019-01-08 20:19:41 +01:00
azivner
ded3f605be fix almost invisible buttons on options page, closes #297 2019-01-08 19:47:35 +01:00
azivner
030d12a465 stretch sync login token validity to 5 minutes #277 2019-01-07 23:29:56 +01:00
azivner
4d15628840 Merge remote-tracking branch 'origin/master' 2019-01-07 23:17:45 +01:00
azivner
81b849898c stretch body to full window width, fixes #276 2019-01-07 23:17:12 +01:00
zadam
3824486b85 Merge pull request #275 from Lee303/Dockerfile-dependency-fix
Update Dockerfile
2019-01-07 22:49:27 +01:00
Lee Spottiswood
081ab00a0a Update Dockerfile 2019-01-07 21:21:23 +00:00
zadam
04f6af5c9a Merge pull request #270 from svenefftinge/master
Make contributions easier
2019-01-07 21:46:02 +01:00
Sven Efftinge
4dc1f1f6eb Added contribute section and gitpod config 2019-01-07 12:52:02 +00:00
azivner
3930a02123 tree now uses standard font size which effectively makes it a bit larger 2019-01-06 20:59:19 +01:00
azivner
3112de105e fancytree selection/hover colors are shades of gray, border is rounded 2019-01-06 18:58:12 +01:00
azivner
3b8d7b8fba release 0.27.3 2019-01-05 22:48:11 +01:00
azivner
9fca7f09a5 link to mobile frontend 2019-01-05 22:45:18 +01:00
azivner
fd39d6b3a9 using btn-secondary instead of btn-default since that doesn't exist in BS4 2019-01-05 21:51:27 +01:00
azivner
a103886ea5 responsive setup page 2019-01-05 21:49:40 +01:00
azivner
373408e401 fix Branch reference to parent note id after parent change 2019-01-05 19:25:22 +01:00
azivner
db44c1d8e6 border color tweaks 2019-01-05 11:49:17 +01:00
azivner
95a34c9e2d small tweaks of icon alignment 2019-01-05 11:41:09 +01:00
azivner
6ce401f260 release 0.27.2-beta 2019-01-04 23:33:32 +01:00
azivner
5d74dcd256 package-lock 2019-01-04 23:33:30 +01:00
azivner
5a9fc1697b electron 4.0.1 2019-01-04 23:32:14 +01:00
azivner
927415838c using enhanceTitle event instead of renderNode per https://github.com/mar10/fancytree/issues/927 2019-01-04 20:18:07 +01:00
azivner
d72fcefdc7 fix sync of deleted notes 2019-01-04 20:10:16 +01:00
azivner
0be173a8f7 lower lastSyncedPull if server max sync ID is lower 2019-01-04 18:58:46 +01:00
azivner
c3913a8735 updating the note cache after sync 2019-01-03 23:27:10 +01:00
azivner
e2dfe1b6de fixed setup page issues which caused wrong choice to be used in the wizard 2019-01-03 22:32:36 +01:00
azivner
fec3e47eb8 serialize binary note content into base64, incremented sync version 2019-01-03 22:13:58 +01:00
azivner
d72efd2450 removed unnecessary logging of data dir which is now logged as part of app info 2019-01-03 21:46:32 +01:00
azivner
ef1c840aa7 tiny fixes 2019-01-02 22:36:06 +01:00
azivner
1581464d8c fix layout - promoted attributes should never grow beyond its content 2019-01-02 20:21:34 +01:00
azivner
9de29584a4 added data directory to options -> about 2019-01-02 20:08:41 +01:00
azivner
9e2e6fb50c fixed glitch where "unhoist" button disappears after renaming the hoisted note 2019-01-02 19:26:01 +01:00
azivner
c85979b66b it should not be possible to add note after hoisted note 2019-01-02 18:59:08 +01:00
azivner
ecdc5865a6 release 0.27.1-beta 2019-01-01 20:54:23 +01:00
azivner
1771ddb787 responsive promoted attributes view and attribute overview to make sure note content is always visible. Fixes #262 2019-01-01 20:53:18 +01:00
azivner
3ab657fe46 created concept of "detail loaded listeners" which allow scripts to execute some action after the note detail has been loaded 2019-01-01 19:32:34 +01:00
azivner
8785dae753 force reload fancytree node when child is not found 2019-01-01 18:27:36 +01:00
azivner
2f1c5b29d4 update eslint 2019-01-01 18:06:52 +01:00
azivner
7135349a10 allow deleting protected notes outside of protected session 2019-01-01 17:04:21 +01:00
azivner
66c639d5e3 frontend API addition to protect current note 2019-01-01 15:39:13 +01:00
azivner
6704b755d8 attribute overview shows only owned attributes 2019-01-01 15:29:58 +01:00
51 changed files with 24870 additions and 13074 deletions

7
.gitpod.yml Normal file
View 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

View File

@@ -17,6 +17,7 @@ RUN set -x \
libtool \
make \
nasm \
libpng-dev \
&& npm install --production \
&& apk del .build-dependencies

View File

@@ -1,7 +1,7 @@
# Trilium Notes
[![Join the chat at https://gitter.im/trilium-notes/Lobby](https://badges.gitter.im/trilium-notes/Lobby.svg)](https://gitter.im/trilium-notes/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases. See [screenshots](https://github.com/zadam/trilium/wiki/Screenshot-tour) for quick overview:
Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases. See [screenshots](https://github.com/zadam/trilium/wiki/Screenshot-tour) for quick overview:
![](https://raw.githubusercontent.com/wiki/zadam/trilium/images/screenshot.png)
@@ -18,6 +18,7 @@ Trilium Notes is a hierarchical note taking application with focus on building l
* [Relation maps](https://github.com/zadam/trilium/wiki/Relation-map) for visualizing notes and their relations
* [Scripting](https://github.com/zadam/trilium/wiki/Scripts) - see [Advanced showcases](https://github.com/zadam/trilium/wiki/Advanced-showcases)
* Scales well in both usability and performance upwards of 100 000 notes
* Touch optimized [mobile frontend](https://github.com/zadam/trilium/wiki/Mobile-frontend) for smartphones and tablets
* [Night theme](https://github.com/zadam/trilium/wiki/Themes)
* [Evernote](https://github.com/zadam/trilium/wiki/Evernote-import) and [Markdown import & export](https://github.com/zadam/trilium/wiki/Markdown)
@@ -33,4 +34,16 @@ 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/)
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
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/zadam/trilium)
Or clone locally and run
```
npm install
npm run start
```

16
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.26.1",
"version": "0.27.2-beta",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -399,9 +399,9 @@
"integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow=="
},
"@types/node": {
"version": "8.10.39",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.39.tgz",
"integrity": "sha512-rE7fktr02J8ybFf6eysife+WF+L4sAHWzw09DgdCebEu+qDwMvv4zl6Bc+825ttGZP73kCKxa3dhJOoGJ8+5mA==",
"version": "10.12.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz",
"integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==",
"dev": true
},
"abab": {
@@ -2375,12 +2375,12 @@
"integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ=="
},
"electron": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-4.0.0.tgz",
"integrity": "sha512-3XPG/3IXlvnT1oe1K6zEushoD0SKbP8xwdrL10EWGe6k2iOV4hSHqJ8vWnR8yZ7VbSXmBRfomEFDNAo/q/cwKw==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/electron/-/electron-4.0.1.tgz",
"integrity": "sha512-kBWDLn1Vq8Tm6+/HpQc8gkjX7wJyQI8v/lf2kAirfi0Q4cXh6vBjozFvV1U/9gGCbyKnIDM+m8/wpyJIjg4w7g==",
"dev": true,
"requires": {
"@types/node": "^8.0.24",
"@types/node": "^10.12.18",
"electron-download": "^4.1.0",
"extract-zip": "^1.0.3"
}

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.27.0-beta",
"version": "0.27.4",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -33,6 +33,7 @@
"electron-in-page-search": "1.3.2",
"express": "4.16.4",
"express-session": "1.15.6",
"file-type": "10.7.0",
"fs-extra": "7.0.1",
"get-port": "4.1.0",
"helmet": "3.15.0",
@@ -66,7 +67,7 @@
},
"devDependencies": {
"devtron": "1.4.0",
"electron": "4.0.0",
"electron": "4.0.1",
"electron-compile": "6.4.3",
"electron-packager": "13.0.1",
"electron-rebuild": "1.8.2",

View File

@@ -15,7 +15,8 @@ const ENTITY_NAME_TO_ENTITY = {
"note_revisions": NoteRevision,
"recent_notes": RecentNote,
"options": Option,
"api_tokens": ApiToken
"api_tokens": ApiToken,
"links": Link
};
function getEntityFromEntityName(entityName) {

View File

@@ -56,6 +56,9 @@ class Note extends Entity {
setContent(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 {
this.jsonContent = JSON.parse(this.content);
}

View File

@@ -36,6 +36,7 @@ import hoistedNoteService from './services/hoisted_note.js';
import noteTypeService from './services/note_type.js';
import linkService from './services/link.js';
import noteAutocompleteService from './services/note_autocomplete.js';
import macInit from './services/mac_init.js';
// required for CKEditor image upload plugin
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 () {
if ($(this).hasClass("disabled")) {
return;
@@ -140,6 +119,8 @@ $("#export-note-button").click(function () {
exportDialog.showDialog('single');
});
macInit.init();
treeService.showTree();
entrypoints.registerEntrypoints();

View File

@@ -6,8 +6,6 @@ const $dialog = $("#jump-to-note-dialog");
const $autoComplete = $("#jump-to-note-autocomplete");
const $showInFullTextButton = $("#show-in-full-text-button");
$dialog.on("shown.bs.modal", e => $autoComplete.focus());
async function showDialog() {
glob.activeDialog = $dialog;

View File

@@ -198,15 +198,17 @@ addTabHandler((async function () {
const $syncVersion = $("#sync-version");
const $buildDate = $("#build-date");
const $buildRevision = $("#build-revision");
const $dataDirectory = $("#data-directory");
const appInfo = await server.get('app-info');
$appVersion.html(appInfo.appVersion);
$dbVersion.html(appInfo.dbVersion);
$syncVersion.html(appInfo.syncVersion);
$buildDate.html(appInfo.buildDate);
$buildRevision.html(appInfo.buildRevision);
$appVersion.text(appInfo.appVersion);
$dbVersion.text(appInfo.dbVersion);
$syncVersion.text(appInfo.syncVersion);
$buildDate.text(appInfo.buildDate);
$buildRevision.text(appInfo.buildRevision);
$buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.buildRevision);
$dataDirectory.text(appInfo.dataDirectory);
return {};
})());

View File

@@ -85,8 +85,11 @@ async function showAttributes() {
$promotedAttributesContainer.empty().append($tbody);
}
else if (note.type !== 'relation-map') {
if (attributes.length > 0) {
for (const attribute of attributes) {
// display only "own" notes
const ownedAttributes = attributes.filter(attr => attr.noteId === note.noteId);
if (ownedAttributes.length > 0) {
for (const attribute of ownedAttributes) {
if (attribute.type === 'label') {
$attributeListInner.append(utils.formatLabel(attribute) + " ");
}
@@ -132,7 +135,9 @@ async function createPromotedAttributeRow(definitionAttr, valueAttr) {
const $inputCell = $("<td>").append($("<div>").addClass("input-group").append($input));
const $actionCell = $("<td>");
const $multiplicityCell = $("<td>").addClass("multiplicity");
const $multiplicityCell = $("<td>")
.addClass("multiplicity")
.attr("nowrap", true);
$tr
.append($labelCell)

View File

@@ -117,10 +117,6 @@ function registerEntrypoints() {
utils.bindShortcut('ctrl+f', openInPageSearch);
if (utils.isMac()) {
utils.bindShortcut('meta+f', openInPageSearch);
}
// FIXME: do we really need these at this point?
utils.bindShortcut("ctrl+shift+up", () => {
const node = treeService.getCurrentNode();

View File

@@ -7,6 +7,7 @@ import treeCache from './tree_cache.js';
import noteDetailService from './note_detail.js';
import noteTypeService from './note_type.js';
import noteTooltipService from './note_tooltip.js';
import protectedSessionService from'./protected_session.js';
/**
* This is the main frontend API interface for scripts. It's published in the local "api" object.
@@ -42,7 +43,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null) {
this.activateNewNote = async notePath => {
await treeService.reload();
await treeService.activateNote(notePath, true);
await treeService.activateNote(notePath, noteDetailService.focusOnTitle);
};
/**
@@ -244,7 +245,12 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null) {
* @method
* @param {object} $el - jquery object on which to setup the tooltip
*/
this.setupElementTooltip = noteTooltipService.setupElementTooltip
this.setupElementTooltip = noteTooltipService.setupElementTooltip;
/**
* @method
*/
this.protectCurrentNote = protectedSessionService.protectNoteAndSendToServer;
}
export default FrontendScriptApi;

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

View File

@@ -28,7 +28,7 @@ function clearText($el) {
function showRecentNotes($el) {
$el.setSelectedPath("");
$el.autocomplete("val", "");
$el.autocomplete("open");
$el.focus();
}
function initNoteAutocomplete($el, options) {
@@ -61,7 +61,13 @@ function initNoteAutocomplete($el, options) {
$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(() => {
if ($el.hasClass("disabled")) {

View File

@@ -37,6 +37,8 @@ let noteChangeDisabled = false;
let isNoteChanged = false;
let detailLoadedListeners = [];
const components = {
'code': noteDetailCode,
'text': noteDetailText,
@@ -147,12 +149,6 @@ function setNoteBackgroundIfProtected(note) {
$unprotectButton.prop("disabled", !protectedSessionHolder.isProtectedSessionAvailable());
}
let isNewNoteCreated = false;
function newNoteCreated() {
isNewNoteCreated = true;
}
async function handleProtectedSession() {
const newSessionCreated = await protectedSessionService.ensureProtectedSession(currentNote.isProtected, false);
@@ -191,12 +187,6 @@ async function loadNoteDetail(noteId) {
attributeService.invalidateAttributes();
}
if (isNewNoteCreated) {
isNewNoteCreated = false;
$noteTitle.focus().select();
}
$noteIdDisplay.html(noteId);
setNoteBackgroundIfProtected(currentNote);
@@ -240,11 +230,13 @@ async function loadNoteDetail(noteId) {
// after loading new note make sure editor is scrolled to the top
getComponent(currentNote.type).scrollToTop();
fireDetailLoaded();
$scriptArea.empty();
await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
if (utils.isDesktop()) {
$scriptArea.empty();
await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
await attributeService.showAttributes();
await showChildrenOverview();
@@ -291,6 +283,30 @@ function focusOnTitle() {
$noteTitle.focus();
}
/**
* Since detail loading may take some time and user might just browse through the notes using UP-DOWN keys,
* we intentionally decouple activation of the note in the tree and full load of the note so just avaiting on
* fancytree's activate() won't wait for the full load.
*
* This causes an issue where in some cases you want to do some action after detail is loaded. For this reason
* we provide the listeners here which will be triggered after the detail is loaded and if the loaded note
* is the one registered in the listener.
*/
function addDetailLoadedListener(noteId, callback) {
detailLoadedListeners.push({ noteId, callback });
}
function fireDetailLoaded() {
for (const {noteId, callback} of detailLoadedListeners) {
if (noteId === currentNote.noteId) {
callback();
}
}
// all the listeners are one time only
detailLoadedListeners = [];
}
messagingService.subscribeToSyncMessages(syncData => {
if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === getCurrentNoteId())) {
infoService.showMessage('Reloading note because of background changes');
@@ -325,11 +341,11 @@ export default {
getCurrentNote,
getCurrentNoteType,
getCurrentNoteId,
newNoteCreated,
focusOnTitle,
saveNote,
saveNoteIfChanged,
noteChanged,
getCurrentNoteContent,
onNoteChange
onNoteChange,
addDetailLoadedListener
};

View File

@@ -1,10 +1,13 @@
import noteDetailService from "./note_detail.js";
import treeService from "./tree.js";
import infoService from './info.js';
const $searchString = $("#search-string");
const $component = $('#note-detail-search');
const $refreshButton = $('#note-detail-search-refresh-results-button');
function getContent() {
JSON.stringify({
return JSON.stringify({
searchString: $searchString.val()
});
}
@@ -25,6 +28,14 @@ function show() {
$searchString.on('input', noteDetailService.noteChanged);
}
$refreshButton.click(async () => {
await noteDetailService.saveNoteIfChanged();
treeService.reload();
infoService.showMessage('Tree has been refreshed.');
});
export default {
getContent,
show,

View File

@@ -184,5 +184,6 @@ export default {
protectSubtree,
ensureDialogIsClosed,
enterProtectedSession,
leaveProtectedSession
leaveProtectedSession,
protectNoteAndSendToServer
};

View File

@@ -87,7 +87,7 @@ $searchInput.keyup(e => {
if (e && e.which === $.ui.keyCode.ENTER) {
doSearch();
}
}).focus();
});
$doSearchButton.click(() => doSearch()); // keep long form because of argument
$resetSearchButton.click(resetSearch);

View File

@@ -83,6 +83,10 @@ async function setNodeTitleWithPrefix(node) {
node.setTitle(utils.escapeHtml(title));
}
function getNode(childNoteId, parentNoteId) {
return getNodesByNoteId(childNoteId).find(node => !parentNoteId || node.data.parentNoteId === parentNoteId);
}
async function expandToNote(notePath, expandOpts) {
utils.assertArguments(notePath);
@@ -90,33 +94,56 @@ async function expandToNote(notePath, expandOpts) {
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
let hoistedNoteFound = false;
let parentNoteId = null;
for (const childNoteId of runPath) {
// for first node (!parentNoteId) it doesn't matter which node is found
const node = getNodesByNoteId(childNoteId).find(node => !parentNoteId || node.data.parentNoteId === parentNoteId);
if (!node) {
console.error(`Can't find node for noteId=${childNoteId} with parentNoteId=${parentNoteId}`);
if (childNoteId === hoistedNoteId) {
hoistedNoteFound = true;
}
if (childNoteId === noteId) {
return node;
}
else {
await node.setExpanded(true, expandOpts);
// 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
let node = getNode(childNoteId, parentNoteId);
if (!node && parentNoteId) {
const parents = getNodesByNoteId(parentNoteId);
for (const parent of parents) {
// force load parents. This is useful when fancytree doesn't contain recently created notes yet.
await parent.load(true);
}
node = getNode(childNoteId, parentNoteId);
}
if (!node) {
console.error(`Can't find node for noteId=${childNoteId} with parentNoteId=${parentNoteId}`);
}
if (childNoteId === noteId) {
return node;
} else {
await node.setExpanded(true, expandOpts);
}
}
parentNoteId = childNoteId;
}
}
async function activateNote(notePath, newNote) {
async function activateNote(notePath, noteLoadedListener) {
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();
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?")) {
return;
}
@@ -131,8 +158,8 @@ async function activateNote(notePath, newNote) {
const node = await expandToNote(notePath);
if (newNote) {
noteDetailService.newNoteCreated();
if (noteLoadedListener) {
noteDetailService.addDetailLoadedListener(node.data.noteId, noteLoadedListener);
}
// we use noFocus because when we reload the tree because of background changes
@@ -337,6 +364,7 @@ function clearSelectedNodes() {
}
async function treeInitialized() {
// - is used in mobile to indicate that we don't want to activate any note after load
if (startNotePath === '-') {
return;
}
@@ -348,7 +376,6 @@ async function treeInitialized() {
startNotePath = null;
}
// - is used in mobile to indicate that we don't want to activate any note after load
if (startNotePath) {
const node = await activateNote(startNotePath);
@@ -411,14 +438,28 @@ function initFancyTree(tree) {
clones: {
highlightActiveClones: true
},
renderNode: async function (event, data) {
enhanceTitle: async function (event, data) {
const node = data.node;
const $span = $(node.span);
if (node.data.noteId !== 'root'
&& node.data.noteId === await hoistedNoteService.getHoistedNoteId()
&& $span.find('.unhoist-button').length === 0) {
if (node.data.noteId !== 'root' && node.data.noteId === await hoistedNoteService.getHoistedNoteId()) {
const unhoistButton = $('<span>&nbsp; (<a class="unhoist-button">unhoist</a>)</span>');
$(node.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();
}
});
}
});
@@ -547,7 +588,7 @@ async function createNote(node, parentNoteId, target, isProtected, saveSelection
await noteDetailService.saveNoteIfChanged();
noteDetailService.newNoteCreated();
noteDetailService.addDetailLoadedListener(note.noteId, noteDetailService.focusOnTitle);
const noteEntity = new NoteShort(treeCache, note);
const branchEntity = new Branch(treeCache, branch);
@@ -634,11 +675,15 @@ messagingService.subscribeToSyncMessages(syncData => {
}
});
utils.bindShortcut('ctrl+o', () => {
utils.bindShortcut('ctrl+o', async () => {
const node = getCurrentNode();
const parentNoteId = node.data.parentNoteId;
const isProtected = treeUtils.getParentProtectedStatus(node);
if (node.data.noteId === 'root' || node.data.noteId === await hoistedNoteService.getHoistedNoteId()) {
return;
}
createNote(node, parentNoteId, 'after', isProtected, true);
});

View File

@@ -86,7 +86,7 @@ async function prepareNode(branch) {
extraClasses: await getExtraClasses(note),
icon: await getIcon(note),
refKey: note.noteId,
expanded: (note.type !== 'search' && branch.isExpanded) || hoistedNoteId === note.noteId
expanded: branch.isExpanded || hoistedNoteId === note.noteId
};
if (note.hasChildren() || note.type === 'search') {

View File

@@ -158,7 +158,11 @@ class TreeCache {
return;
}
treeCache.childParentToBranch[childNoteId + '-' + newParentNoteId] = treeCache.childParentToBranch[childNoteId + '-' + oldParentNoteId];
const branchId = treeCache.childParentToBranch[childNoteId + '-' + oldParentNoteId];
const branch = await this.getBranch(branchId);
branch.parentNoteId = newParentNoteId;
treeCache.childParentToBranch[childNoteId + '-' + newParentNoteId] = branchId;
delete treeCache.childParentToBranch[childNoteId + '-' + oldParentNoteId]; // this is correct because we know that oldParentId isn't same as newParentId
// remove old associations

View File

@@ -13,8 +13,6 @@ import syncService from "./sync.js";
import hoistedNoteService from './hoisted_note.js';
import ContextMenuItemsContainer from './context_menu_items_container.js';
const $tree = $("#tree");
let clipboardIds = [];
let clipboardMode = null;
@@ -110,11 +108,12 @@ async function getContextMenuItems(event) {
const note = await treeCache.getNote(node.data.noteId);
const parentNote = await treeCache.getNote(branch.parentNoteId);
const isNotRoot = note.noteId !== 'root';
const isHoisted = note.noteId === await hoistedNoteService.getHoistedNoteId();
const itemsContainer = new ContextMenuItemsContainer(contextMenuItems);
// Modify menu entries depending on node status
itemsContainer.enableItem("insertNoteAfter", isNotRoot && parentNote.type !== 'search');
itemsContainer.enableItem("insertNoteAfter", isNotRoot && !isHoisted && parentNote.type !== 'search');
itemsContainer.enableItem("insertChildNote", note.type !== 'search');
itemsContainer.enableItem("delete", isNotRoot && parentNote.type !== 'search');
itemsContainer.enableItem("copy", isNotRoot);
@@ -125,10 +124,8 @@ async function getContextMenuItems(event) {
itemsContainer.enableItem("export", note.type !== 'search');
itemsContainer.enableItem("editBranchPrefix", isNotRoot && parentNote.type !== 'search');
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
itemsContainer.hideItem("hoist", note.noteId === hoistedNoteId);
itemsContainer.hideItem("unhoist", note.noteId !== hoistedNoteId || !isNotRoot);
itemsContainer.hideItem("hoist", isHoisted);
itemsContainer.hideItem("unhoist", !isHoisted || !isNotRoot);
// Activate node on right-click
node.setActive();

View File

@@ -137,6 +137,11 @@ function randomString(len) {
function bindShortcut(keyboardShortcut, handler) {
if (isDesktop()) {
if (isMac()) {
// use CMD (meta) instead of CTRL for all shortcuts
keyboardShortcut = keyboardShortcut.replace("ctrl", "meta");
}
$(document).bind('keydown', keyboardShortcut, e => {
handler();

View File

@@ -1,4 +1,7 @@
import utils from "./services/utils.js";
import macInit from './services/mac_init.js';
macInit.init();
function SetupModel() {
if (syncInProgress) {
@@ -25,26 +28,20 @@ function SetupModel() {
this.instanceType = utils.isElectron() ? "desktop" : "server";
this.setupTypeSelected = this.getSetupType = () =>
this.setupNewDocument()
|| this.setupSyncFromDesktop()
|| this.setupSyncFromServer();
this.setupTypeSelected = () => !!this.setupType();
this.selectSetupType = () => {
this.step(this.getSetupType());
this.setupType(this.getSetupType());
this.step(this.setupType());
};
this.back = () => {
this.step("setup-type");
this.setupNewDocument(false);
this.setupSyncFromServer(false);
this.setupSyncFromDesktop(false);
this.setupType("");
};
this.finish = async () => {
if (this.setupNewDocument()) {
if (this.setupType() === 'new-document') {
const username = this.username();
const password1 = this.password1();
const password2 = this.password2();
@@ -72,7 +69,7 @@ function SetupModel() {
window.location.replace("/");
});
}
else if (this.setupSyncFromServer()) {
else if (this.setupType() === 'sync-from-server') {
const syncServerHost = this.syncServerHost();
const syncProxy = this.syncProxy();
const username = this.username();

File diff suppressed because one or more lines are too long

View File

@@ -45,14 +45,24 @@
#header button {
padding: 1px 5px 1px 5px;
font-size: small;
margin-bottom: 2px;
margin-top: 2px;
margin-right: 8px;
}
#history-navigation {
margin: 0 15px 0 5px;
position: relative;
top: 2px;
}
#global-buttons {
display: flex;
justify-content: space-around;
padding: 10px 0 10px 0;
margin: 0 10px 0 16px;
border: 1px solid #ccc;
margin: 0 20px 0 10px;
border: 1px solid #ddd;
border-radius: 7px;
}
#context-menu-container {

View File

@@ -2,6 +2,7 @@ body {
/* Fix for CKEditor block gutter icon "stretching" body and causing scrollbar to appear after pressing enter
on the last line of the editor. */
position: fixed;
width: 100%;
}
#title-container {
@@ -16,6 +17,16 @@ body {
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 {
background: none;
width: auto;
@@ -70,6 +81,7 @@ body {
height: 100%;
display: flex;
flex-direction: column;
min-height: 200px;
}
.note-detail-component {
@@ -130,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 */
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;
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 {
@@ -154,17 +179,6 @@ span.fancytree-active:not(.fancytree-focused) .fancytree-title {
color: #337ab7 !important;
}
#header-title {
padding: 5px 20px 5px 10px;
font-size: large;
font-weight: bold;
}
#header .btn-sm {
margin-bottom: 2px;
margin-right: 8px;
}
div.ui-tooltip {
max-width: 600px;
max-height: 600px;
@@ -311,6 +325,12 @@ div.ui-tooltip {
.cm-matchhighlight {background-color: #eeeeee}
#attribute-list {
overflow: auto;
/* limiting the size since actual note content is more important */
max-height: 30%;
}
#label-list, #relation-list, #attribute-list {
color: #777777;
padding: 5px;
@@ -370,13 +390,8 @@ div.ui-tooltip {
height: 150px;
}
#history-navigation {
margin: 0 20px 0 5px;
display: flex;
}
.btn:not(.btn-primary):not(.btn-danger) {
border-color: #bbb;
.btn:not(.btn-primary):not(.btn-secondary):not(.btn-danger) {
border-color: #ddd;
background-color: #eee;
}
@@ -434,8 +449,13 @@ html.theme-dark body {
}
#note-detail-promoted-attributes {
max-width: 70%;
margin: auto;
/* setting the display to block since "table" doesn't support scrolling */
display: block;
flex-basis: content;
flex-shrink: 1;
flex-grow: 0;
overflow: auto;
}
#note-detail-promoted-attributes td, #note-detail-promoted-attributes th {
@@ -532,7 +552,8 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
max-height: 300px;
overflow: hidden;
color: black;
border: 1px solid #aaa;
border: 1px solid #ccc;
border-radius: 5px;
text-align: left;
}

View File

@@ -7,6 +7,7 @@ const tarImportService = require('../../services/import/tar');
const singleImportService = require('../../services/import/single');
const cls = require('../../services/cls');
const path = require('path');
const noteCacheService = require('../../services/note_cache');
async function importToBranch(req) {
const parentNoteId = req.params.parentNoteId;
@@ -28,24 +29,32 @@ async function importToBranch(req) {
// and may produce unintended consequences
cls.disableEntityEvents();
let note; // typically root of the import - client can show it after finishing the import
if (extension === '.tar') {
return await tarImportService.importTar(file.buffer, parentNote);
note = await tarImportService.importTar(file.buffer, parentNote);
}
else if (extension === '.opml') {
return await opmlImportService.importOpml(file.buffer, parentNote);
note = await opmlImportService.importOpml(file.buffer, parentNote);
}
else if (extension === '.md') {
return await singleImportService.importMarkdown(file, parentNote);
note = await singleImportService.importMarkdown(file, parentNote);
}
else if (extension === '.html' || extension === '.htm') {
return await singleImportService.importHtml(file, parentNote);
note = await singleImportService.importHtml(file, parentNote);
}
else if (extension === '.enex') {
return await enexImportService.importEnex(file, parentNote);
note = await enexImportService.importEnex(file, parentNote);
}
else {
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 = {

View File

@@ -10,6 +10,7 @@ const appInfo = require('../../services/app_info');
const eventService = require('../../services/events');
const cls = require('../../services/cls');
const sqlInit = require('../../services/sql_init');
const sql = require('../../services/sql');
async function loginSync(req) {
if (!await sqlInit.schemaExists()) {
@@ -22,7 +23,8 @@ async function loginSync(req) {
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' }];
}
@@ -44,7 +46,8 @@ async function loginSync(req) {
req.session.loggedIn = true;
return {
sourceId: sourceIdService.getCurrentSourceId()
sourceId: sourceIdService.getCurrentSourceId(),
maxSyncId: await sql.getValue("SELECT MAX(id) FROM sync")
};
}

View File

@@ -19,7 +19,6 @@ async function anonymize() {
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 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
('documentSecret', 'encryptedDataKey', 'passwordVerificationHash',
'passwordVerificationSalt', 'passwordDerivedKeySalt')`);

View File

@@ -2,14 +2,16 @@
const build = require('./build');
const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir');
const APP_DB_VERSION = 121;
const SYNC_VERSION = 2;
const SYNC_VERSION = 3;
module.exports = {
appVersion: packageJson.version,
dbVersion: APP_DB_VERSION,
syncVersion: SYNC_VERSION,
buildDate: build.buildDate,
buildRevision: build.buildRevision
buildRevision: build.buildRevision,
dataDirectory: TRILIUM_DATA_DIR
};

View File

@@ -1 +1 @@
module.exports = { buildDate:"2018-12-30T22:38:11+01:00", buildRevision: "32220476aa6795bab036b7dd9057ea3357d7dd51" };
module.exports = { buildDate:"2019-01-10T21:31:30+01:00", buildRevision: "0b251530fa0ee61edc8dcc9235033abb73afc614" };

View File

@@ -12,6 +12,7 @@ const Attribute = require('../entities/attribute');
const NoteRevision = require('../entities/note_revision');
const RecentNote = require('../entities/recent_note');
const Option = require('../entities/option');
const Link = require('../entities/link');
async function getHash(entityConstructor, whereBranch) {
// subselect is necessary to have correct ordering in GROUP_CONCAT
@@ -37,7 +38,8 @@ async function getHashes() {
recent_notes: await getHash(RecentNote),
options: await getHash(Option, "isSynced = 1"),
attributes: await getHash(Attribute),
api_tokens: await getHash(ApiToken)
api_tokens: await getHash(ApiToken),
links: await getHash(Link)
};
const elapseTimeMs = new Date().getTime() - startTime.getTime();

View File

@@ -55,10 +55,6 @@ function getTriliumDataDir() {
}
const TRILIUM_DATA_DIR = getTriliumDataDir();
// not necessary to log this since if we have logs we already know where data dir is.
console.log("Using data dir:", TRILIUM_DATA_DIR);
const DOCUMENT_PATH = TRILIUM_DATA_DIR + "/document.db";
const BACKUP_DIR = TRILIUM_DATA_DIR + "/backup";
const LOG_DIR = TRILIUM_DATA_DIR + "/log";

View File

@@ -5,13 +5,24 @@ const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION";
const ENTITY_CREATED = "ENTITY_CREATED";
const ENTITY_CHANGED = "ENTITY_CHANGED";
const ENTITY_DELETED = "ENTITY_DELETED";
const ENTITY_SYNCED = "ENTITY_SYNCED";
const CHILD_NOTE_CREATED = "CHILD_NOTE_CREATED";
const eventListeners = {};
function subscribe(eventType, listener) {
eventListeners[eventType] = eventListeners[eventType] || [];
eventListeners[eventType].push(listener);
/**
* @param eventTypes - can be either single event or an array of events
* @param listener
*/
function subscribe(eventTypes, listener) {
if (!Array.isArray(eventTypes)) {
eventTypes = [ eventTypes ];
}
for (const eventType of eventTypes) {
eventListeners[eventType] = eventListeners[eventType] || [];
eventListeners[eventType].push(listener);
}
}
async function emit(eventType, data) {
@@ -39,5 +50,6 @@ module.exports = {
ENTITY_CREATED,
ENTITY_CHANGED,
ENTITY_DELETED,
ENTITY_SYNCED,
CHILD_NOTE_CREATED
};

View File

@@ -1,6 +1,7 @@
"use strict";
const repository = require('./repository');
const log = require('./log');
const protectedSessionService = require('./protected_session');
const noteService = require('./notes');
const imagemin = require('imagemin');
@@ -13,7 +14,13 @@ const sanitizeFilename = require('sanitize-filename');
async function saveImage(buffer, originalName, parentNoteId) {
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);

View File

@@ -1,4 +1,5 @@
const sax = require("sax");
const fileType = require('file-type');
const stream = require('stream');
const xml2js = require('xml2js');
const log = require("../log");
@@ -144,7 +145,7 @@ async function importEnex(file, parentNote) {
});
}
else if (currentTag === 'mime') {
resource.mime = text;
resource.mime = text.toLowerCase();
if (text.startsWith("image/")) {
resource.title = "image";
@@ -222,7 +223,26 @@ async function importEnex(file, parentNote) {
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 { 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
note.content += imageLink;
}
} catch (e) {
log.error("error when saving image from ENEX file: " + e);
await createResourceNote();
}
}
else {
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);
await createResourceNote();
}
}

View File

@@ -45,8 +45,6 @@ function request(req) {
logger.info(req.method + " " + req.url);
}
info("Using data dir: " + dataDir.TRILIUM_DATA_DIR);
module.exports = {
info,
error,

View File

@@ -52,13 +52,15 @@ async function sendMessage(client, message) {
async function sendMessageToAllClients(message) {
const jsonStr = JSON.stringify(message);
log.info("Sending message to all clients: " + jsonStr);
if (webSocketServer) {
log.info("Sending message to all clients: " + jsonStr);
webSocketServer.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(jsonStr);
}
});
webSocketServer.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(jsonStr);
}
});
}
}
async function sendPing(client, lastSentSyncId) {

View File

@@ -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'`);
if (protectedSessionService.isProtectedSessionAvailable()) {
await loadProtectedNotes();
}
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) {
// we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks
// which would make the resulting HTML string invalid.
@@ -299,7 +311,9 @@ function getNotePath(noteId) {
}
}
eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entity}) => {
eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED, eventService.ENTITY_SYNCED], async ({entityName, entity}) => {
// note that entity can also be just POJO without methods if coming from sync
if (!loaded) {
return;
}
@@ -312,7 +326,16 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entity})
delete childToParent[note.noteId];
}
else {
noteTitles[note.noteId] = note.title;
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 {
noteTitles[note.noteId] = note.title;
delete protectedNoteTitles[note.noteId];
}
}
}
else if (entityName === 'branches') {
@@ -353,15 +376,9 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entity})
}
});
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, async () => {
if (!loaded) {
return;
}
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]);
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
if (loaded) {
loadProtectedNotes();
}
});
@@ -370,5 +387,6 @@ sqlInit.dbReady.then(() => utils.stopWatch("Autocomplete load", load));
module.exports = {
findNotes,
getNotePath,
getNoteTitleForPath
getNoteTitleForPath,
load
};

View File

@@ -1,4 +1,5 @@
const sql = require('./sql');
const sqlInit = require('./sql_init');
const optionService = require('./options');
const dateUtils = require('./date_utils');
const syncTableService = require('./sync_table');
@@ -153,7 +154,8 @@ async function createNote(parentNoteId, title, content = "", extraOptions = {})
noteId: note.noteId,
type: attr.type,
name: attr.name,
value: attr.value
value: attr.value,
isInheritable: !!attr.isInheritable
});
}
@@ -357,10 +359,14 @@ async function deleteNote(branch) {
const notDeletedBranches = await note.getBranches();
if (notDeletedBranches.length === 0) {
note.isDeleted = true;
// we don't reset content here, that's postponed and done later to give the user
// a chance to correct a mistake
await note.save();
// maybe a bit counter-intuitively, protected notes can be deleted also outside of protected session
// this is because protected notes offer only confidentiality which makes some things simpler - e.g. deletion UI
// to allow this, we just set the isDeleted flag, otherwise saving would fail because of attempt to encrypt
// content with non-existent protected session key
// we don't reset content here, that's postponed and done later to give the user a chance to correct a mistake
await sql.execute("UPDATE notes SET isDeleted = 1 WHERE noteId = ?", [note.noteId]);
// need to manually trigger sync since it's not taken care of by note save
await syncTableService.addNoteSync(note.noteId);
for (const noteRevision of await note.getRevisions()) {
await noteRevision.save();
@@ -403,10 +409,12 @@ async function cleanupDeletedNotes() {
await sql.execute("UPDATE note_revisions SET content = NULL WHERE note_revisions.content IS NOT NULL AND noteId IN (SELECT noteId FROM notes WHERE isDeleted = 1 AND notes.dateModified <= ?)", [dateUtils.dateStr(cutoffDate)]);
}
// first cleanup kickoff 5 minutes after startup
setTimeout(cls.wrap(cleanupDeletedNotes), 5 * 60 * 1000);
sqlInit.dbReady.then(() => {
// first cleanup kickoff 5 minutes after startup
setTimeout(cls.wrap(cleanupDeletedNotes), 5 * 60 * 1000);
setInterval(cls.wrap(cleanupDeletedNotes), 4 * 3600 * 1000);
setInterval(cls.wrap(cleanupDeletedNotes), 4 * 3600 * 1000);
});
module.exports = {
createNewNote,

View File

@@ -6,7 +6,7 @@ const sourceIdService = require('./source_id');
const log = require('./log');
async function executeNote(note, originEntity) {
if (!note.isJavaScript()) {
if (!note.isJavaScript() || !note.isContentAvailable) {
return;
}
@@ -80,6 +80,10 @@ function getParams(params) {
}
async function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = []) {
if (!note.isContentAvailable) {
return;
}
if (!note.isJavaScript() && !note.isHtml()) {
return;
}

View File

@@ -1,6 +1,5 @@
"use strict";
const url = require('url');
const log = require('./log');
const sql = require('./sql');
const sqlInit = require('./sql_init');
@@ -99,6 +98,16 @@ async function doLogin() {
syncContext.sourceId = resp.sourceId;
const lastSyncedPull = await getLastSyncedPull();
// this is important in a scenario where we setup the sync by manually copying the document
// lastSyncedPull then could be pretty off for the newly cloned client
if (lastSyncedPull > resp.maxSyncId) {
log.info(`Lowering last synced pull from ${lastSyncedPull} to ${resp.maxSyncId}`);
await setLastSyncedPull(resp.maxSyncId);
}
return syncContext;
}
@@ -256,7 +265,7 @@ async function getEntityRow(entityName, entityId) {
&& entity.content !== null
&& (entity.type === 'file' || entity.type === 'image')) {
entity.content = entity.content.toString("binary");
entity.content = entity.content.toString("base64");
}
return entity;

View File

@@ -2,6 +2,7 @@ const sql = require('./sql');
const log = require('./log');
const eventLogService = require('./event_log');
const syncTableService = require('./sync_table');
const eventService = require('./events');
async function updateEntity(sync, entity, sourceId) {
const {entityName} = sync;
@@ -36,11 +37,20 @@ async function updateEntity(sync, entity, sourceId) {
else {
throw new Error(`Unrecognized entity type ${entityName}`);
}
// currently making exception for protected notes and note revisions because here
// the title and content are not available decrypted as listeners would expect
if ((entityName !== 'notes' && entityName !== 'note_revisions') || !entity.isProtected) {
await eventService.emit(eventService.ENTITY_SYNCED, {
entityName,
entity
});
}
}
function deserializeNoteContentBuffer(note) {
if (note.content !== null && (note.type === 'file' || note.type === 'image')) {
note.content = new Buffer(note.content, 'binary');
note.content = Buffer.from(note.content, 'base64');
}
}

View File

@@ -10,7 +10,7 @@
<div id="history-navigation" style="display: none;">
<a id="history-back-button" title="Go to previous note." class="icon-action jam jam-arrow-square-left"></a>
&nbsp; &nbsp;
&nbsp;
<a id="history-forward-button" title="Go to next note." class="icon-action jam jam-arrow-square-right"></a>
</div>

View File

@@ -1,7 +1,12 @@
<div id="note-detail-search" class="note-detail-component">
<div style="display: flex; align-items: center;">
<strong>Search string: &nbsp; &nbsp;</strong>
<textarea rows="4" cols="50" id="search-string"></textarea>
<textarea rows="4" cols="40" id="search-string"></textarea>
<span>
&nbsp; &nbsp;
<button type="button" class="btn btn-primary" id="note-detail-search-refresh-results-button">Refresh tree</button>
</span>
</div>
<br />

View File

@@ -14,7 +14,7 @@
<div id="confirm-dialog-custom"></div>
</div>
<div class="modal-footer">
<button class="btn btn-default btn-sm" id="confirm-dialog-cancel-button">Cancel</button>
<button class="btn btn-secondary btn-sm" id="confirm-dialog-cancel-button">Cancel</button>
&nbsp;

View File

@@ -108,7 +108,7 @@
<div style="display: flex; justify-content: space-between;">
<button class="btn btn-primary">Save</button>
<button class="btn btn-default" type="button" data-help-page="Protected-notes">Help</button>
<button class="btn btn-secondary" type="button" data-help-page="Protected-notes">Help</button>
</div>
</form>
</div>
@@ -125,7 +125,7 @@
<div style="display: flex; justify-content: space-between;">
<button class="btn btn-primary">Save</button>
<button class="btn btn-default" type="button" data-help-page="Note-revisions">Help</button>
<button class="btn btn-secondary" type="button" data-help-page="Note-revisions">Help</button>
</div>
</form>
</div>
@@ -154,7 +154,7 @@
<div style="display: flex; justify-content: space-between;">
<button class="btn btn-primary">Save</button>
<button class="btn btn-default" type="button" data-help-page="Synchronization">Help</button>
<button class="btn btn-secondary" type="button" data-help-page="Synchronization">Help</button>
</div>
</form>
@@ -164,24 +164,24 @@
<p>This will test connection and handshake to the sync server. If sync server isn't initialized, this will set it up to sync with local document.</p>
<button id="test-sync-button" class="btn btn-default">Test sync</button>
<button id="test-sync-button" class="btn btn-secondary">Test sync</button>
</div>
<div id="advanced" class="tab-pane">
<h4 style="margin-top: 0px;">Sync</h4>
<button id="force-full-sync-button" class="btn btn-default">Force full sync</button>
<button id="force-full-sync-button" class="btn btn-secondary">Force full sync</button>
<br/>
<br/>
<button id="fill-sync-rows-button" class="btn btn-default">Fill sync rows</button>
<button id="fill-sync-rows-button" class="btn btn-secondary">Fill sync rows</button>
<br/>
<br/>
<h4>Debugging</h4>
<button id="anonymize-button" class="btn btn-default">Save anonymized database</button><br/><br/>
<button id="anonymize-button" class="btn btn-secondary">Save anonymized database</button><br/><br/>
<p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and metadata)
for sharing online for debugging purposes without fear of leaking your personal data.</p>
@@ -190,7 +190,7 @@
<p>This will rebuild database which will typically result in smaller database file. No data will be actually changed.</p>
<button id="vacuum-database-button" class="btn btn-default">Vacuum database</button>
<button id="vacuum-database-button" class="btn btn-secondary">Vacuum database</button>
</div>
<div id="about" class="tab-pane">
@@ -216,6 +216,11 @@
<th>Build revision:</th>
<td><a href="" target="_blank" id="build-revision"></a></td>
</tr>
<tr>
<th>Data directory:</th>
<td id="data-directory"></td>
</tr>
</table>
</div>
</div>

View File

@@ -7,7 +7,7 @@
</head>
<body>
<div class="container">
<div class="col-md-5 offset-md-3" style="padding-top: 25px;">
<div class="col-xs-12 col-sm-10 col-md-6 col-lg-4 col-xl-4 mx-auto" style="padding-top: 25px;">
<h1>Trilium login</h1>
<% if (failedAuth) { %>
@@ -60,6 +60,8 @@
device = /Mobi/.test(navigator.userAgent) ? "mobile" : "desktop";
}
console.log("Setting device cookie to:", device);
setCookie("trilium-device", device);
function setCookie(name, value) {

View File

@@ -2,113 +2,116 @@
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Setup</title>
</head>
<body>
<div id="setup-dialog" style="width: 700px; margin: auto; padding-top: 50px; display:none; font-size: larger;">
<h1>Trilium Notes setup</h1>
<div class="container">
<div id="setup-dialog" class="col-md-12 col-lg-8 col-xl-6 mx-auto" style="padding-top: 25px;">
<h1>Trilium Notes setup</h1>
<div class="alert alert-warning" id="alert" style="display: none;">
</div>
<div id="setup-type" data-bind="visible: step() == 'setup-type'" style="margin-top: 20px;">
<div class="radio" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="new-document" data-bind="checked: setupNewDocument">
I'm a new user and I want to create new Trilium document for my notes</label>
</div>
<div class="radio" data-bind="if: instanceType == 'server'" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupSyncFromDesktop">
I have desktop instance already and I want to setup sync with it</label>
</div>
<div class="radio" data-bind="if: instanceType == 'desktop'" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupSyncFromServer">
I have server instance up and I want to setup sync with it</label>
<div class="alert alert-warning" id="alert" style="display: none;">
</div>
<button type="button" data-bind="disable: !setupTypeSelected(), click: selectSetupType" class="btn btn-primary">Next</button>
</div>
<div id="setup-type" data-bind="visible: step() == 'setup-type'" style="margin-top: 20px;">
<div class="radio" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="new-document" data-bind="checked: setupType">
I'm a new user and I want to create new Trilium document for my notes</label>
</div>
<div class="radio" data-bind="if: instanceType == 'server'" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupType">
I have desktop instance already and I want to setup sync with it</label>
</div>
<div class="radio" data-bind="if: instanceType == 'desktop'" style="margin-bottom: 15px;">
<label><input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupType">
I have server instance up and I want to setup sync with it</label>
</div>
<div data-bind="visible: step() == 'new-document'">
<h2>New document</h2>
<p>You're almost done with the setup. The last thing is to choose username and password using which you'll login to the application.
This password is also used for generating encryption key which encrypts protected notes.</p>
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" data-bind="value: username" placeholder="Choose alphanumeric username">
</div>
<div class="form-group">
<label for="password1">Password</label>
<input type="password" class="form-control" data-bind="value: password1" placeholder="Password">
</div>
<div class="form-group">
<label for="password2">Repeat password</label>
<input type="password" class="form-control" data-bind="value: password2" placeholder="Password">
<button type="button" data-bind="disable: !setupTypeSelected(), click: selectSetupType" class="btn btn-primary">Next</button>
</div>
<button type="button" data-bind="click: back" class="btn btn-default">Back</button>
<div data-bind="visible: step() == 'new-document'">
<h2>New document</h2>
&nbsp;
<p>You're almost done with the setup. The last thing is to choose username and password using which you'll login to the application.
This password is also used for generating encryption key which encrypts protected notes.</p>
<button type="button" data-bind="click: finish" class="btn btn-primary">Finish setup</button>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" data-bind="value: username" placeholder="Choose alphanumeric username">
</div>
<div class="form-group">
<label for="password1">Password</label>
<input type="password" class="form-control" data-bind="value: password1" placeholder="Password">
</div>
<div class="form-group">
<label for="password2">Repeat password</label>
<input type="password" class="form-control" data-bind="value: password2" placeholder="Password">
</div>
<div data-bind="visible: step() == 'sync-from-desktop'">
<h2>Sync from Desktop</h2>
<button type="button" data-bind="click: back" class="btn btn-secondary">Back</button>
<p>This setup needs to be initiated from the desktop instance:</p>
&nbsp;
<ol>
<li>please open your desktop instance of Trilium Notes</li>
<li>click on Options button in the top right</li>
<li>click on Sync tab</li>
<li>configure server instance address to the: <span id="current-host"></span> and click save.</li>
<li>click on "Test sync" button</li>
<li>once you've done all this, click <a href="/">here</a></li>
</ol>
<button type="button" data-bind="click: back" class="btn btn-default">Back</button>
</div>
<div data-bind="visible: step() == 'sync-from-server'">
<h2>Sync from Server</h2>
<p>Please enter Trilium server address and credentials below. This will download the whole Trilium document from server and setup sync to it. Depending on the document size and your connection speed, this may take a while.</p>
<div class="form-group">
<label for="sync-server-host">Trilium server address</label>
<input type="text" id="syncServerHost" class="form-control" data-bind="value: syncServerHost" placeholder="https://<hostname>:<port>">
</div>
<div class="form-group">
<label for="sync-proxy">Proxy server (optional)</label>
<input type="text" id="sync-proxy" class="form-control" data-bind="value: syncProxy" placeholder="https://<hostname>:<port>">
<p><strong>Note:</strong> If you leave proxy setting blank, system proxy will be used (applies to desktop/electron build only)</p>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" class="form-control" data-bind="value: username" placeholder="Username">
</div>
<div class="form-group">
<label for="password1">Password</label>
<input type="password" id="password1" class="form-control" data-bind="value: password1" placeholder="Password">
<button type="button" data-bind="click: finish" class="btn btn-primary">Finish setup</button>
</div>
<button type="button" data-bind="click: back" class="btn btn-default">Back</button>
<div data-bind="visible: step() == 'sync-from-desktop'">
<h2>Sync from Desktop</h2>
&nbsp;
<p>This setup needs to be initiated from the desktop instance:</p>
<button type="button" data-bind="click: finish" class="btn btn-primary">Finish setup</button>
</div>
<ol>
<li>please open your desktop instance of Trilium Notes</li>
<li>click on Options button in the top right</li>
<li>click on Sync tab</li>
<li>configure server instance address to the: <span id="current-host"></span> and click save.</li>
<li>click on "Test sync" button</li>
<li>once you've done all this, click <a href="/">here</a></li>
</ol>
<div data-bind="visible: step() == 'sync-in-progress'">
<h2>Sync in progress</h2>
<button type="button" data-bind="click: back" class="btn btn-secondary">Back</button>
</div>
<div class="alert alert-success">Sync has been correctly set up. It will take some time for the initial sync to finish. Once it's done, you'll be redirected to the login page.</div>
<div data-bind="visible: step() == 'sync-from-server'">
<h2>Sync from Server</h2>
<div data-bind="if: instanceType == 'desktop'">
Outstanding sync items: <strong id="outstanding-syncs">N/A</strong>
<p>Please enter Trilium server address and credentials below. This will download the whole Trilium document from server and setup sync to it. Depending on the document size and your connection speed, this may take a while.</p>
<div class="form-group">
<label for="sync-server-host">Trilium server address</label>
<input type="text" id="syncServerHost" class="form-control" data-bind="value: syncServerHost" placeholder="https://<hostname>:<port>">
</div>
<div class="form-group">
<label for="sync-proxy">Proxy server (optional)</label>
<input type="text" id="sync-proxy" class="form-control" data-bind="value: syncProxy" placeholder="https://<hostname>:<port>">
<p><strong>Note:</strong> If you leave proxy setting blank, system proxy will be used (applies to desktop/electron build only)</p>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" class="form-control" data-bind="value: username" placeholder="Username">
</div>
<div class="form-group">
<label for="password1">Password</label>
<input type="password" id="password1" class="form-control" data-bind="value: password1" placeholder="Password">
</div>
<button type="button" data-bind="click: back" class="btn btn-secondary">Back</button>
&nbsp;
<button type="button" data-bind="click: finish" class="btn btn-primary">Finish setup</button>
</div>
<div data-bind="visible: step() == 'sync-in-progress'">
<h2>Sync in progress</h2>
<div class="alert alert-success">Sync has been correctly set up. It will take some time for the initial sync to finish. Once it's done, you'll be redirected to the login page.</div>
<div data-bind="if: instanceType == 'desktop'">
Outstanding sync items: <strong id="outstanding-syncs">N/A</strong>
</div>
</div>
</div>
</div>