Compare commits

...

36 Commits

Author SHA1 Message Date
azivner
fd02c6102d release 0.6.2 2018-02-15 23:08:02 -05:00
azivner
30c712a6be updated package-lock 2018-02-15 23:07:58 -05:00
azivner
3928c96640 electron update to 1.8.2 stable 2018-02-15 23:05:18 -05:00
azivner
d86f655658 attempt to mitigate problem with creating day subnotes 2018-02-15 23:04:50 -05:00
azivner
abdad1c3ae log error messages with ERROR: prefix (there's wasn't anyt other distinction before) 2018-02-15 22:30:05 -05:00
azivner
9e5f1a0a87 Global shortcut registration logs failure, closes #47 2018-02-15 22:17:18 -05:00
azivner
8028b09351 release 0.6.1 2018-02-13 23:27:34 -05:00
azivner
ebe66eaed9 after creating new note, unselect previous active, fixes #45 2018-02-13 23:25:28 -05:00
azivner
5bce9a5f94 added hide_in_autocomplete attribute to weight script 2018-02-13 22:50:12 -05:00
azivner
dfd9927310 added createAttribute method to script API 2018-02-13 22:46:45 -05:00
azivner
9bf1735bde reddit notes will be created with "hide_in_autocomplete" attribute 2018-02-13 22:34:33 -05:00
azivner
2e8eeda5ab new attribute "hide_in_autocomplete", fixes #16 2018-02-13 22:30:33 -05:00
azivner
1cef0ce5f9 removed CTRL-ALT-C global shortcut with pasting from clipboard, keeping only CTRL-ALT-P without clipboard 2018-02-13 19:55:04 -05:00
azivner
1efac99828 limit number of autocomplete results to 100, closes #44 2018-02-13 19:37:07 -05:00
azivner
0e9473119e global keyboard shortcuts for quick creating sub-notes under day note 2018-02-12 23:53:00 -05:00
azivner
7bbfef7af3 better positioning of the recent notes dialog 2018-02-12 21:20:30 -05:00
azivner
5cb93509c1 stop trying to wrap autocomplete with underlying dialog - seems to be impossible to get it right on all platforms 2018-02-12 21:09:50 -05:00
azivner
89e89e04d8 alt+t is now shortcut for today script 2018-02-12 00:30:02 -05:00
azivner
72df0d8861 release 0.6.0-beta 2018-02-11 22:06:57 -05:00
azivner
9910aebf45 fix schema.sql 2018-02-11 22:06:12 -05:00
azivner
f9f8ecb2b1 recent notes doesn't fail totally when we can't find title for some note 2018-02-11 15:33:10 -05:00
azivner
438f7c5b0b escape should close the recent notes dialog 2018-02-11 11:53:43 -05:00
azivner
4b1d1aba74 add sender API to send text notes 2018-02-11 10:54:56 -05:00
azivner
6dea73cfe2 sender API now accepts local time header so we don't have problems with UTC 2018-02-11 09:14:21 -05:00
azivner
58f5d0cf6e recent notes are not closed when I click on e.g. dialog title bar 2018-02-11 08:57:12 -05:00
azivner
7b77e40514 added support for trilium-sender 2018-02-11 00:18:59 -05:00
azivner
660908c54b fix sorting notes 2018-02-10 13:55:06 -05:00
azivner
e970564036 create months and days with associated english names, closes #37 2018-02-10 13:53:35 -05:00
azivner
b3038487f8 fix image support broken in recent refactorings 2018-02-10 10:00:40 -05:00
azivner
cac98392a6 code mirror in SQL console, closes #24 2018-02-10 09:14:18 -05:00
azivner
dbd28377e3 change in naming conventions for element variables from *El to $name 2018-02-10 08:44:34 -05:00
azivner
c76e4faf5d added attributes sorting 2018-02-10 08:37:14 -05:00
azivner
e011b9ae63 deleting attributes, closes #34 2018-02-06 23:09:19 -05:00
azivner
7c74c77a2c allow duplicated attribute per note (in effect attributes can be multi-valued). Closes #33 2018-02-06 21:18:09 -05:00
azivner
c2a2f195aa Merge branch 'stable' 2018-02-06 21:04:27 -05:00
azivner
4e70cebf70 recent notes now use autocomplete instead of select box, closes #36 2018-02-05 23:50:25 -05:00
56 changed files with 839 additions and 509 deletions

View File

@@ -10,6 +10,7 @@ Trilium Notes is a hierarchical note taking application. Picture tells a thousan
* WYSIWYG (What You See Is What You Get) editing
* Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation)
* Seamless note versioning
* Note attributes can be used to tag/label notes as an alternative note organization and querying
* Can be deployed as web application and / or desktop application with offline access (electron based)
* [Synchronization with](https://github.com/zadam/trilium/wiki/Synchronization) self-hosted sync server
* Strong [note encryption](https://github.com/zadam/trilium/wiki/Protected-notes)
@@ -34,6 +35,7 @@ List of documentation pages:
* [Installation as webapp](https://github.com/zadam/trilium/wiki/Installation-as-webapp)
* [Note navigation](https://github.com/zadam/trilium/wiki/Note-navigation)
* [Tree manipulation](https://github.com/zadam/trilium/wiki/Tree-manipulation)
* [Attributes](https://github.com/zadam/trilium/wiki/Attributes)
* [Links](https://github.com/zadam/trilium/wiki/Links)
* [Cloning notes](https://github.com/zadam/trilium/wiki/Cloning-notes)
* [Protected notes](https://github.com/zadam/trilium/wiki/Protected-notes)

View File

@@ -0,0 +1 @@
DROP INDEX IDX_attributes_noteId_name;

View File

@@ -0,0 +1 @@
ALTER TABLE attributes ADD COLUMN isDeleted INT NOT NULL DEFAULT 0;

View File

@@ -0,0 +1 @@
ALTER TABLE attributes ADD COLUMN position INT NOT NULL DEFAULT 0;

View File

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS "api_tokens"
(
apiTokenId TEXT PRIMARY KEY NOT NULL,
token TEXT NOT NULL,
dateCreated TEXT NOT NULL,
isDeleted INT NOT NULL DEFAULT 0
);

View File

@@ -0,0 +1 @@
CREATE INDEX IDX_attributes_name_value ON attributes (name, value);

View File

@@ -85,8 +85,10 @@ CREATE TABLE IF NOT EXISTS "attributes"
noteId TEXT NOT NULL,
name TEXT NOT NULL,
value TEXT,
position INT NOT NULL DEFAULT 0,
dateCreated TEXT NOT NULL,
dateModified TEXT NOT NULL
dateModified TEXT NOT NULL,
isDeleted INT NOT NULL
);
CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
`entityName`,
@@ -118,4 +120,12 @@ CREATE INDEX IDX_note_images_noteId ON note_images (noteId);
CREATE INDEX IDX_note_images_imageId ON note_images (imageId);
CREATE INDEX IDX_note_images_noteId_imageId ON note_images (noteId, imageId);
CREATE INDEX IDX_attributes_noteId ON attributes (noteId);
CREATE UNIQUE INDEX IDX_attributes_noteId_name ON attributes (noteId, name);
CREATE INDEX IDX_attributes_name_value ON attributes (name, value);
CREATE TABLE IF NOT EXISTS "api_tokens"
(
apiTokenId TEXT PRIMARY KEY NOT NULL,
token TEXT NOT NULL,
dateCreated TEXT NOT NULL,
isDeleted INT NOT NULL DEFAULT 0
);

View File

@@ -3,9 +3,11 @@
const electron = require('electron');
const path = require('path');
const config = require('./src/services/config');
const log = require('./src/services/log');
const url = require("url");
const app = electron.app;
const globalShortcut = electron.globalShortcut;
// Adds debug features like hotkeys for triggering dev tools and reload
require('electron-debug')();
@@ -67,6 +69,26 @@ app.on('activate', () => {
app.on('ready', () => {
mainWindow = createMainWindow();
const result = globalShortcut.register('CommandOrControl+Alt+P', async () => {
const date_notes = require('./src/services/date_notes');
const utils = require('./src/services/utils');
const parentNoteId = await date_notes.getDateNoteId(utils.nowDate());
// window may be hidden / not in focus
mainWindow.focus();
mainWindow.webContents.send('create-day-sub-note', parentNoteId);
});
if (!result) {
log.error("Could not register global shortcut CTRL+ALT+P");
}
});
app.on('will-quit', () => {
globalShortcut.unregisterAll();
});
require('./src/www');

78
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.4.1",
"version": "0.6.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -3061,19 +3061,19 @@
"integrity": "sha1-zIcsFoiArjxxiXYv1f/ACJbJUYo="
},
"electron": {
"version": "1.8.2-beta.4",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.8.2-beta.4.tgz",
"integrity": "sha1-GDayBO6s6dx3Bi7Ugg/bxsvZoZU=",
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.8.2.tgz",
"integrity": "sha512-0TV5Hy92g8ACnPn+PVol6a/2uk+khzmRtWxhah/FcKs6StCytm5hD14QqOdZxEdJN8HljXIVCayN/wJX+0wDiQ==",
"requires": {
"@types/node": "8.5.9",
"@types/node": "8.9.4",
"electron-download": "3.3.0",
"extract-zip": "1.6.5"
},
"dependencies": {
"@types/node": {
"version": "8.5.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.5.9.tgz",
"integrity": "sha512-s+c3AjymyAccTI4hcgNFK4mToH8l+hyPDhu4LIkn71lRy56FLijGu00fyLgldjM/846Pmk9N4KFUs2P8GDs0pA=="
"version": "8.9.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.4.tgz",
"integrity": "sha512-dSvD36qnQs78G1BPsrZFdPpvLgMW/dnvr5+nTW2csMs5TiP9MOXrjUbnMZOEwnIuBklXtn7b6TPA2Cuq07bDHA=="
}
}
},
@@ -3325,9 +3325,9 @@
}
},
"electron-packager": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-10.1.1.tgz",
"integrity": "sha1-MWp/ossf/CYz9YBcn8IJE8vAnZQ=",
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-11.0.1.tgz",
"integrity": "sha1-wtH/nsqBEL6evIGCbiqSHATRIA4=",
"dev": true,
"requires": {
"asar": "0.14.0",
@@ -3343,13 +3343,19 @@
"pify": "3.0.0",
"plist": "2.1.0",
"pruner": "0.0.7",
"rcedit": "0.9.0",
"rcedit": "1.0.0",
"resolve": "1.4.0",
"sanitize-filename": "1.6.1",
"semver": "5.4.1",
"yargs-parser": "8.1.0"
"yargs-parser": "9.0.2"
},
"dependencies": {
"camelcase": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
"integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
"dev": true
},
"electron-download": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/electron-download/-/electron-download-4.1.0.tgz",
@@ -3437,6 +3443,12 @@
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true
},
"rcedit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/rcedit/-/rcedit-1.0.0.tgz",
"integrity": "sha512-W7DNa34x/3OgWyDHsI172AG/Lr/lZ+PkavFkHj0QhhkBRcV9QTmRJE1tDKrWkx8XHPSBsmZkNv9OKue6pncLFQ==",
"dev": true
},
"sumchecker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-2.0.2.tgz",
@@ -3456,20 +3468,29 @@
}
}
}
},
"yargs-parser": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz",
"integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=",
"dev": true,
"requires": {
"camelcase": "4.1.0"
}
}
}
},
"electron-prebuilt-compile": {
"version": "1.8.2-beta.4",
"resolved": "https://registry.npmjs.org/electron-prebuilt-compile/-/electron-prebuilt-compile-1.8.2-beta.4.tgz",
"integrity": "sha512-whVdRgFEDovWSFrAsbMXIiush6RQ8IV3XhYdL59zShck4U1eXGmdkaBCy+2tlkGmUGr0fRu+S4FpUx2ebBkRhQ==",
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/electron-prebuilt-compile/-/electron-prebuilt-compile-1.8.2.tgz",
"integrity": "sha512-wiDVjy8S0PA/K/TUM0lw5gzZ+SmyVVGQ0qt9iFYXHJc6t8TzDXFY3DsoK37H3A7nWnkvXvoPdpJ5/h9KbTMoAw==",
"dev": true,
"requires": {
"babel-plugin-array-includes": "2.0.3",
"babel-plugin-transform-async-to-generator": "6.24.1",
"babel-preset-es2016-node5": "1.1.2",
"babel-preset-react": "6.24.1",
"electron": "1.8.2-beta.4",
"electron": "1.8.2",
"electron-compile": "6.4.2",
"electron-compilers": "5.9.0",
"yargs": "6.6.0"
@@ -8472,12 +8493,6 @@
}
}
},
"rcedit": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/rcedit/-/rcedit-0.9.0.tgz",
"integrity": "sha1-ORDfVzRTmeKwMl9KUZAH+J5V7xw=",
"dev": true
},
"read-all-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz",
@@ -11694,23 +11709,6 @@
}
}
},
"yargs-parser": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz",
"integrity": "sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==",
"dev": true,
"requires": {
"camelcase": "4.1.0"
},
"dependencies": {
"camelcase": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
"integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
"dev": true
}
}
},
"yauzl": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "trilium",
"description": "Trilium Notes",
"version": "0.5.6",
"version": "0.6.2",
"license": "AGPL-3.0-only",
"main": "electron.js",
"repository": {
@@ -27,7 +27,7 @@
"debug": "~3.1.0",
"devtron": "^1.4.0",
"ejs": "~2.5.7",
"electron": "^1.8.2-beta.4",
"electron": "^1.8.2",
"electron-debug": "^1.5.0",
"electron-in-page-search": "^1.2.4",
"express": "~4.16.2",
@@ -60,8 +60,8 @@
},
"devDependencies": {
"electron-compile": "^6.4.2",
"electron-packager": "^10.1.1",
"electron-prebuilt-compile": "1.8.2-beta.4",
"electron-packager": "^11.0.1",
"electron-prebuilt-compile": "1.8.2",
"electron-rebuild": "^1.7.3",
"tape": "^4.8.0",
"xo": "^0.18.0"

View File

@@ -24,7 +24,7 @@ class Note extends Entity {
}
async getAttributes() {
return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ?", [this.noteId]);
return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
}
async getAttribute(name) {

View File

@@ -38,6 +38,7 @@ async function getDateNoteIdForReddit(dateTimeStr, rootNoteId) {
redditDateNoteId = await createNote(dateNoteId, "Reddit");
await attributes.createAttribute(redditDateNoteId, REDDIT_DATE_ATTRIBUTE, dateStr);
await attributes.createAttribute(redditDateNoteId, "hide_in_autocomplete");
}
return redditDateNoteId;

View File

@@ -1,18 +1,18 @@
"use strict";
const addLink = (function() {
const dialogEl = $("#add-link-dialog");
const formEl = $("#add-link-form");
const autoCompleteEl = $("#note-autocomplete");
const linkTitleEl = $("#link-title");
const clonePrefixEl = $("#clone-prefix");
const linkTitleFormGroup = $("#add-link-title-form-group");
const prefixFormGroup = $("#add-link-prefix-form-group");
const linkTypeEls = $("input[name='add-link-type']");
const linkTypeHtmlEl = linkTypeEls.filter('input[value="html"]');
const $dialog = $("#add-link-dialog");
const $form = $("#add-link-form");
const $autoComplete = $("#note-autocomplete");
const $linkTitle = $("#link-title");
const $clonePrefix = $("#clone-prefix");
const $linkTitleFormGroup = $("#add-link-title-form-group");
const $prefixFormGroup = $("#add-link-prefix-form-group");
const $linkTypes = $("input[name='add-link-type']");
const $linkTypeHtml = $linkTypes.filter('input[value="html"]');
function setLinkType(linkType) {
linkTypeEls.each(function () {
$linkTypes.each(function () {
$(this).prop('checked', $(this).val() === linkType);
});
@@ -20,39 +20,39 @@ const addLink = (function() {
}
function showDialog() {
glob.activeDialog = dialogEl;
glob.activeDialog = $dialog;
if (noteEditor.getCurrentNoteType() === 'text') {
linkTypeHtmlEl.prop('disabled', false);
$linkTypeHtml.prop('disabled', false);
setLinkType('html');
}
else {
linkTypeHtmlEl.prop('disabled', true);
$linkTypeHtml.prop('disabled', true);
setLinkType('selected-to-current');
}
dialogEl.dialog({
$dialog.dialog({
modal: true,
width: 700
});
autoCompleteEl.val('').focus();
clonePrefixEl.val('');
linkTitleEl.val('');
$autoComplete.val('').focus();
$clonePrefix.val('');
$linkTitle.val('');
function setDefaultLinkTitle(noteId) {
const noteTitle = noteTree.getNoteTitle(noteId);
linkTitleEl.val(noteTitle);
$linkTitle.val(noteTitle);
}
autoCompleteEl.autocomplete({
$autoComplete.autocomplete({
source: noteTree.getAutocompleteItems(),
minLength: 0,
change: () => {
const val = autoCompleteEl.val();
const val = $autoComplete.val();
const notePath = link.getNodePathFromLabel(val);
if (!notePath) {
return;
@@ -75,8 +75,8 @@ const addLink = (function() {
});
}
formEl.submit(() => {
const value = autoCompleteEl.val();
$form.submit(() => {
const value = $autoComplete.val();
const notePath = link.getNodePathFromLabel(value);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
@@ -85,25 +85,25 @@ const addLink = (function() {
const linkType = $("input[name='add-link-type']:checked").val();
if (linkType === 'html') {
const linkTitle = linkTitleEl.val();
const linkTitle = $linkTitle.val();
dialogEl.dialog("close");
$dialog.dialog("close");
link.addLinkToEditor(linkTitle, '#' + notePath);
}
else if (linkType === 'selected-to-current') {
const prefix = clonePrefixEl.val();
const prefix = $clonePrefix.val();
cloning.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix);
dialogEl.dialog("close");
$dialog.dialog("close");
}
else if (linkType === 'current-to-selected') {
const prefix = clonePrefixEl.val();
const prefix = $clonePrefix.val();
cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix);
dialogEl.dialog("close");
$dialog.dialog("close");
}
}
@@ -111,19 +111,19 @@ const addLink = (function() {
});
function linkTypeChanged() {
const value = linkTypeEls.filter(":checked").val();
const value = $linkTypes.filter(":checked").val();
if (value === 'html') {
linkTitleFormGroup.show();
prefixFormGroup.hide();
$linkTitleFormGroup.show();
$prefixFormGroup.hide();
}
else {
linkTitleFormGroup.hide();
prefixFormGroup.show();
$linkTitleFormGroup.hide();
$prefixFormGroup.show();
}
}
linkTypeEls.change(linkTypeChanged);
$linkTypes.change(linkTypeChanged);
$(document).bind('keydown', 'ctrl+l', e => {
showDialog();

View File

@@ -1,8 +1,10 @@
"use strict";
const attributesDialog = (function() {
const dialogEl = $("#attributes-dialog");
const saveAttributesButton = $("#save-attributes-button");
const $dialog = $("#attributes-dialog");
const $saveAttributesButton = $("#save-attributes-button");
const $attributesBody = $('#attributes-table tbody');
const attributesModel = new AttributesModel();
let attributeNames = [];
@@ -24,11 +26,40 @@ const attributesDialog = (function() {
// attribute might not be rendered immediatelly so could not focus
setTimeout(() => $(".attribute-name:last").focus(), 100);
$attributesBody.sortable({
handle: '.handle',
containment: $attributesBody,
update: function() {
let position = 0;
// we need to update positions by searching in the DOM, because order of the
// attributes in the viewmodel (self.attributes()) stays the same
$attributesBody.find('input[name="position"]').each(function() {
const attr = self.getTargetAttribute(this);
attr().position = position++;
});
}
});
};
this.deleteAttribute = function(data, event) {
const attr = self.getTargetAttribute(event.target);
const attrData = attr();
if (attrData) {
attrData.isDeleted = 1;
attr(attrData);
addLastEmptyRow();
}
};
function isValid() {
for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) {
if (self.isEmptyName(i) || self.isNotUnique(i)) {
if (self.isEmptyName(i)) {
return false;
}
}
@@ -40,7 +71,7 @@ const attributesDialog = (function() {
// we need to defocus from input (in case of enter-triggered save) because value is updated
// on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
saveAttributesButton.focus();
$saveAttributesButton.focus();
if (!isValid()) {
alert("Please fix all validation errors and try saving again.");
@@ -65,26 +96,26 @@ const attributesDialog = (function() {
};
function addLastEmptyRow() {
const attrs = self.attributes();
const attrs = self.attributes().filter(attr => attr().isDeleted === 0);
const last = attrs.length === 0 ? null : attrs[attrs.length - 1]();
if (!last || last.name.trim() !== "" || last.value !== "") {
self.attributes.push(ko.observable({
attributeId: '',
name: '',
value: ''
value: '',
isDeleted: 0,
position: 0
}));
}
}
this.attributeChanged = function (row) {
this.attributeChanged = function (data, event) {
addLastEmptyRow();
for (const attr of self.attributes()) {
if (row.attributeId === attr().attributeId) {
attr.valueHasMutated();
}
}
const attr = self.getTargetAttribute(event.target);
attr.valueHasMutated();
};
this.isNotUnique = function(index) {
@@ -109,15 +140,22 @@ const attributesDialog = (function() {
const cur = self.attributes()[index]();
return cur.name.trim() === "" && (cur.attributeId !== "" || cur.value !== "");
};
this.getTargetAttribute = function(target) {
const context = ko.contextFor(target);
const index = context.$index();
return self.attributes()[index];
}
}
async function showDialog() {
glob.activeDialog = dialogEl;
glob.activeDialog = $dialog;
await attributesModel.loadAttributes();
dialogEl.dialog({
$dialog.dialog({
modal: true,
width: 800,
height: 500

View File

@@ -1,17 +1,17 @@
"use strict";
const editTreePrefix = (function() {
const dialogEl = $("#edit-tree-prefix-dialog");
const formEl = $("#edit-tree-prefix-form");
const treePrefixInputEl = $("#tree-prefix-input");
const noteTitleEl = $('#tree-prefix-note-title');
const $dialog = $("#edit-tree-prefix-dialog");
const $form = $("#edit-tree-prefix-form");
const $treePrefixInput = $("#tree-prefix-input");
const $noteTitle = $('#tree-prefix-note-title');
let noteTreeId;
async function showDialog() {
glob.activeDialog = dialogEl;
glob.activeDialog = $dialog;
await dialogEl.dialog({
await $dialog.dialog({
modal: true,
width: 500
});
@@ -20,21 +20,21 @@ const editTreePrefix = (function() {
noteTreeId = currentNode.data.noteTreeId;
treePrefixInputEl.val(currentNode.data.prefix).focus();
$treePrefixInput.val(currentNode.data.prefix).focus();
const noteTitle = noteTree.getNoteTitle(currentNode.data.noteId);
noteTitleEl.html(noteTitle);
$noteTitle.html(noteTitle);
}
formEl.submit(() => {
const prefix = treePrefixInputEl.val();
$form.submit(() => {
const prefix = $treePrefixInput.val();
server.put('tree/' + noteTreeId + '/set-prefix', {
prefix: prefix
}).then(() => noteTree.setPrefix(noteTreeId, prefix));
dialogEl.dialog("close");
$dialog.dialog("close");
return false;
});

View File

@@ -1,13 +1,13 @@
"use strict";
const eventLog = (function() {
const dialogEl = $("#event-log-dialog");
const listEl = $("#event-log-list");
const $dialog = $("#event-log-dialog");
const $list = $("#event-log-list");
async function showDialog() {
glob.activeDialog = dialogEl;
glob.activeDialog = $dialog;
dialogEl.dialog({
$dialog.dialog({
modal: true,
width: 800,
height: 700
@@ -15,7 +15,7 @@ const eventLog = (function() {
const result = await server.get('event-log');
listEl.html('');
$list.html('');
for (const event of result) {
const dateTime = formatDateTime(parseDate(event.dateAdded));
@@ -28,7 +28,7 @@ const eventLog = (function() {
const eventEl = $('<li>').html(dateTime + " - " + event.comment);
listEl.append(eventEl);
$list.append(eventEl);
}
}

View File

@@ -1,28 +1,28 @@
"use strict";
const jumpToNote = (function() {
const dialogEl = $("#jump-to-note-dialog");
const autoCompleteEl = $("#jump-to-note-autocomplete");
const formEl = $("#jump-to-note-form");
const $dialog = $("#jump-to-note-dialog");
const $autoComplete = $("#jump-to-note-autocomplete");
const $form = $("#jump-to-note-form");
async function showDialog() {
glob.activeDialog = dialogEl;
glob.activeDialog = $dialog;
autoCompleteEl.val('');
$autoComplete.val('');
dialogEl.dialog({
$dialog.dialog({
modal: true,
width: 800
});
await autoCompleteEl.autocomplete({
await $autoComplete.autocomplete({
source: await stopWatch("building autocomplete", noteTree.getAutocompleteItems),
minLength: 0
});
}
function getSelectedNotePath() {
const val = autoCompleteEl.val();
const val = $autoComplete.val();
return link.getNodePathFromLabel(val);
}
@@ -32,7 +32,7 @@ const jumpToNote = (function() {
if (notePath) {
noteTree.activateNode(notePath);
dialogEl.dialog('close');
$dialog.dialog('close');
}
}
@@ -42,8 +42,8 @@ const jumpToNote = (function() {
e.preventDefault();
});
formEl.submit(() => {
const action = dialogEl.find("button:focus").val();
$form.submit(() => {
const action = $dialog.find("button:focus").val();
goToNote();

View File

@@ -1,10 +1,10 @@
"use strict";
const noteHistory = (function() {
const dialogEl = $("#note-history-dialog");
const listEl = $("#note-history-list");
const contentEl = $("#note-history-content");
const titleEl = $("#note-history-title");
const $dialog = $("#note-history-dialog");
const $list = $("#note-history-list");
const $content = $("#note-history-content");
const $title = $("#note-history-title");
let historyItems = [];
@@ -13,23 +13,23 @@ const noteHistory = (function() {
}
async function showNoteHistoryDialog(noteId, noteRevisionId) {
glob.activeDialog = dialogEl;
glob.activeDialog = $dialog;
dialogEl.dialog({
$dialog.dialog({
modal: true,
width: 800,
height: 700
});
listEl.empty();
contentEl.empty();
$list.empty();
$content.empty();
historyItems = await server.get('notes-history/' + noteId);
for (const item of historyItems) {
const dateModified = parseDate(item.dateModifiedFrom);
listEl.append($('<option>', {
$list.append($('<option>', {
value: item.noteRevisionId,
text: formatDateTime(dateModified)
}));
@@ -37,13 +37,13 @@ const noteHistory = (function() {
if (historyItems.length > 0) {
if (!noteRevisionId) {
noteRevisionId = listEl.find("option:first").val();
noteRevisionId = $list.find("option:first").val();
}
listEl.val(noteRevisionId).trigger('change');
$list.val(noteRevisionId).trigger('change');
}
else {
titleEl.text("No history for this note yet...");
$title.text("No history for this note yet...");
}
}
@@ -53,13 +53,13 @@ const noteHistory = (function() {
e.preventDefault();
});
listEl.on('change', () => {
const optVal = listEl.find(":selected").val();
$list.on('change', () => {
const optVal = $list.find(":selected").val();
const historyItem = historyItems.find(r => r.noteRevisionId === optVal);
titleEl.html(historyItem.title);
contentEl.html(historyItem.content);
$title.html(historyItem.title);
$content.html(historyItem.content);
});
$(document).on('click', "a[action='note-history']", event => {

View File

@@ -1,13 +1,13 @@
"use strict";
const noteSource = (function() {
const dialogEl = $("#note-source-dialog");
const noteSourceEl = $("#note-source");
const $dialog = $("#note-source-dialog");
const $noteSource = $("#note-source");
function showDialog() {
glob.activeDialog = dialogEl;
glob.activeDialog = $dialog;
dialogEl.dialog({
$dialog.dialog({
modal: true,
width: 800,
height: 500
@@ -15,7 +15,7 @@ const noteSource = (function() {
const noteText = noteEditor.getCurrentNote().detail.content;
noteSourceEl.text(formatHtml(noteText));
$noteSource.text(formatHtml(noteText));
}
function formatHtml(str) {

View File

@@ -1,12 +1,12 @@
"use strict";
const recentChanges = (function() {
const dialogEl = $("#recent-changes-dialog");
const $dialog = $("#recent-changes-dialog");
async function showDialog() {
glob.activeDialog = dialogEl;
glob.activeDialog = $dialog;
dialogEl.dialog({
$dialog.dialog({
modal: true,
width: 800,
height: 700
@@ -14,7 +14,7 @@ const recentChanges = (function() {
const result = await server.get('recent-changes/');
dialogEl.html('');
$dialog.html('');
const groupedByDate = groupByDate(result);
@@ -48,7 +48,7 @@ const recentChanges = (function() {
.append(' (').append(revLink).append(')'));
}
dialogEl.append(dayEl);
$dialog.append(dayEl);
}
}

View File

@@ -1,13 +1,9 @@
"use strict";
const recentNotes = (function() {
const dialogEl = $("#recent-notes-dialog");
const selectBoxEl = $('#recent-notes-select-box');
const jumpToButtonEl = $('#recent-notes-jump-to');
const addLinkButtonEl = $('#recent-notes-add-link');
const addCurrentAsChildEl = $("#recent-notes-add-current-as-child");
const addRecentAsChildEl = $("#recent-notes-add-recent-as-child");
const noteDetailEl = $('#note-detail');
const $dialog = $("#recent-notes-dialog");
const $searchInput = $('#recent-notes-search-input');
// list of recent note paths
let list = [];
@@ -29,98 +25,67 @@ const recentNotes = (function() {
}
function showDialog() {
glob.activeDialog = dialogEl;
glob.activeDialog = $dialog;
dialogEl.dialog({
$dialog.dialog({
modal: true,
width: 800
width: 800,
height: 100,
position: { my: "center top+100", at: "top", of: window }
});
selectBoxEl.find('option').remove();
$searchInput.val('');
// remove the current note
const recNotes = list.filter(note => note !== noteTree.getCurrentNotePath());
$.each(recNotes, (key, valueNotePath) => {
const noteTitle = noteTree.getNotePathTitle(valueNotePath);
$searchInput.autocomplete({
source: recNotes.map(notePath => {
let noteTitle;
const option = $("<option></option>")
.attr("value", valueNotePath)
.text(noteTitle);
try {
noteTitle = noteTree.getNotePathTitle(notePath);
}
catch (e) {
noteTitle = "[error - can't find note title]";
// select the first one (most recent one) by default
if (key === 0) {
option.attr("selected", "selected");
messaging.logError("Could not find title for notePath=" + notePath + ", stack=" + e.stack);
}
return {
label: noteTitle,
value: notePath
}
}),
minLength: 0,
autoFocus: true,
select: function (event, ui) {
noteTree.activateNode(ui.item.value);
$searchInput.autocomplete('destroy');
$dialog.dialog('close');
},
focus: function (event, ui) {
event.preventDefault();
},
close: function (event, ui) {
if (event.keyCode === 27) { // escape closes dialog
$searchInput.autocomplete('destroy');
$dialog.dialog('close');
}
else {
// keep autocomplete open
// we're kind of abusing autocomplete to work in a way which it's not designed for
$searchInput.autocomplete("search", "");
}
},
create: () => $searchInput.autocomplete("search", ""),
classes: {
"ui-autocomplete": "recent-notes-autocomplete"
}
selectBoxEl.append(option);
});
}
function getSelectedNotePath() {
return selectBoxEl.find("option:selected").val();
}
function getSelectedNoteId() {
const notePath = getSelectedNotePath();
return treeUtils.getNoteIdFromNotePath(notePath);
}
function setActiveNoteBasedOnRecentNotes() {
const notePath = getSelectedNotePath();
noteTree.activateNode(notePath);
dialogEl.dialog('close');
}
function addLinkBasedOnRecentNotes() {
const notePath = getSelectedNotePath();
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
const linkTitle = noteTree.getNoteTitle(noteId);
dialogEl.dialog("close");
link.addLinkToEditor(linkTitle, '#' + notePath);
}
async function addCurrentAsChild() {
await cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), getSelectedNoteId());
dialogEl.dialog("close");
}
async function addRecentAsChild() {
await cloning.cloneNoteTo(getSelectedNoteId(), noteEditor.getCurrentNoteId());
dialogEl.dialog("close");
}
selectBoxEl.keydown(e => {
const key = e.which;
// to get keycodes use http://keycode.info/
if (key === 13)// the enter key code
{
setActiveNoteBasedOnRecentNotes();
}
else if (key === 76 /* l */) {
addLinkBasedOnRecentNotes();
}
else if (key === 67 /* c */) {
addCurrentAsChild();
}
else if (key === 82 /* r */) {
addRecentAsChild()
}
else {
return; // avoid prevent default
}
e.preventDefault();
});
reload();
$(document).bind('keydown', 'ctrl+e', e => {
@@ -129,15 +94,6 @@ const recentNotes = (function() {
e.preventDefault();
});
selectBoxEl.dblclick(e => {
setActiveNoteBasedOnRecentNotes();
});
jumpToButtonEl.click(setActiveNoteBasedOnRecentNotes);
addLinkButtonEl.click(addLinkBasedOnRecentNotes);
addCurrentAsChildEl.click(addCurrentAsChild);
addRecentAsChildEl.click(addRecentAsChild);
return {
showDialog,
addRecentNote,

View File

@@ -1,8 +1,8 @@
"use strict";
const settings = (function() {
const dialogEl = $("#settings-dialog");
const tabsEl = $("#settings-tabs");
const $dialog = $("#settings-dialog");
const $tabs = $("#settings-tabs");
const settingModules = [];
@@ -11,16 +11,16 @@ const settings = (function() {
}
async function showDialog() {
glob.activeDialog = dialogEl;
glob.activeDialog = $dialog;
const settings = await server.get('settings');
dialogEl.dialog({
$dialog.dialog({
modal: true,
width: 900
});
tabsEl.tabs();
$tabs.tabs();
for (const module of settingModules) {
if (module.settingsLoaded) {
@@ -46,22 +46,22 @@ const settings = (function() {
})();
settings.addModule((function() {
const formEl = $("#change-password-form");
const oldPasswordEl = $("#old-password");
const newPassword1El = $("#new-password1");
const newPassword2El = $("#new-password2");
const $form = $("#change-password-form");
const $oldPassword = $("#old-password");
const $newPassword1 = $("#new-password1");
const $newPassword2 = $("#new-password2");
function settingsLoaded(settings) {
}
formEl.submit(() => {
const oldPassword = oldPasswordEl.val();
const newPassword1 = newPassword1El.val();
const newPassword2 = newPassword2El.val();
$form.submit(() => {
const oldPassword = $oldPassword.val();
const newPassword1 = $newPassword1.val();
const newPassword2 = $newPassword2.val();
oldPasswordEl.val('');
newPassword1El.val('');
newPassword2El.val('');
$oldPassword.val('');
$newPassword1.val('');
$newPassword2.val('');
if (newPassword1 !== newPassword2) {
alert("New passwords are not the same.");
@@ -92,16 +92,16 @@ settings.addModule((function() {
})());
settings.addModule((function() {
const formEl = $("#protected-session-timeout-form");
const protectedSessionTimeoutEl = $("#protected-session-timeout-in-seconds");
const $form = $("#protected-session-timeout-form");
const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds");
const settingName = 'protected_session_timeout';
function settingsLoaded(settings) {
protectedSessionTimeoutEl.val(settings[settingName]);
$protectedSessionTimeout.val(settings[settingName]);
}
formEl.submit(() => {
const protectedSessionTimeout = protectedSessionTimeoutEl.val();
$form.submit(() => {
const protectedSessionTimeout = $protectedSessionTimeout.val();
settings.saveSettings(settingName, protectedSessionTimeout).then(() => {
protected_session.setProtectedSessionTimeout(protectedSessionTimeout);
@@ -116,16 +116,16 @@ settings.addModule((function() {
})());
settings.addModule((function () {
const formEl = $("#history-snapshot-time-interval-form");
const timeIntervalEl = $("#history-snapshot-time-interval-in-seconds");
const $form = $("#history-snapshot-time-interval-form");
const $timeInterval = $("#history-snapshot-time-interval-in-seconds");
const settingName = 'history_snapshot_time_interval';
function settingsLoaded(settings) {
timeIntervalEl.val(settings[settingName]);
$timeInterval.val(settings[settingName]);
}
formEl.submit(() => {
settings.saveSettings(settingName, timeIntervalEl.val());
$form.submit(() => {
settings.saveSettings(settingName, $timeInterval.val());
return false;
});
@@ -136,50 +136,50 @@ settings.addModule((function () {
})());
settings.addModule((async function () {
const appVersionEl = $("#app-version");
const dbVersionEl = $("#db-version");
const buildDateEl = $("#build-date");
const buildRevisionEl = $("#build-revision");
const $appVersion = $("#app-version");
const $dbVersion = $("#db-version");
const $buildDate = $("#build-date");
const $buildRevision = $("#build-revision");
const appInfo = await server.get('app-info');
appVersionEl.html(appInfo.app_version);
dbVersionEl.html(appInfo.db_version);
buildDateEl.html(appInfo.build_date);
buildRevisionEl.html(appInfo.build_revision);
buildRevisionEl.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.build_revision);
$appVersion.html(appInfo.app_version);
$dbVersion.html(appInfo.db_version);
$buildDate.html(appInfo.build_date);
$buildRevision.html(appInfo.build_revision);
$buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.build_revision);
return {};
})());
settings.addModule((async function () {
const forceFullSyncButton = $("#force-full-sync-button");
const fillSyncRowsButton = $("#fill-sync-rows-button");
const anonymizeButton = $("#anonymize-button");
const cleanupSoftDeletedButton = $("#cleanup-soft-deleted-items-button");
const cleanupUnusedImagesButton = $("#cleanup-unused-images-button");
const vacuumDatabaseButton = $("#vacuum-database-button");
const $forceFullSyncButton = $("#force-full-sync-button");
const $fillSyncRowsButton = $("#fill-sync-rows-button");
const $anonymizeButton = $("#anonymize-button");
const $cleanupSoftDeletedButton = $("#cleanup-soft-deleted-items-button");
const $cleanupUnusedImagesButton = $("#cleanup-unused-images-button");
const $vacuumDatabaseButton = $("#vacuum-database-button");
forceFullSyncButton.click(async () => {
$forceFullSyncButton.click(async () => {
await server.post('sync/force-full-sync');
showMessage("Full sync triggered");
});
fillSyncRowsButton.click(async () => {
$fillSyncRowsButton.click(async () => {
await server.post('sync/fill-sync-rows');
showMessage("Sync rows filled successfully");
});
anonymizeButton.click(async () => {
$anonymizeButton.click(async () => {
await server.post('anonymization/anonymize');
showMessage("Created anonymized database");
});
cleanupSoftDeletedButton.click(async () => {
$cleanupSoftDeletedButton.click(async () => {
if (confirm("Do you really want to clean up soft-deleted items?")) {
await server.post('cleanup/cleanup-soft-deleted-items');
@@ -187,7 +187,7 @@ settings.addModule((async function () {
}
});
cleanupUnusedImagesButton.click(async () => {
$cleanupUnusedImagesButton.click(async () => {
if (confirm("Do you really want to clean up unused images?")) {
await server.post('cleanup/cleanup-unused-images');
@@ -195,7 +195,7 @@ settings.addModule((async function () {
}
});
vacuumDatabaseButton.click(async () => {
$vacuumDatabaseButton.click(async () => {
await server.post('cleanup/vacuum-database');
showMessage("Database has been vacuumed");

View File

@@ -1,24 +1,44 @@
"use strict";
const sqlConsole = (function() {
const dialogEl = $("#sql-console-dialog");
const queryEl = $('#sql-console-query');
const executeButton = $('#sql-console-execute');
const resultHeadEl = $('#sql-console-results thead');
const resultBodyEl = $('#sql-console-results tbody');
const $dialog = $("#sql-console-dialog");
const $query = $('#sql-console-query');
const $executeButton = $('#sql-console-execute');
const $resultHead = $('#sql-console-results thead');
const $resultBody = $('#sql-console-results tbody');
let codeEditor;
function showDialog() {
glob.activeDialog = dialogEl;
glob.activeDialog = $dialog;
dialogEl.dialog({
$dialog.dialog({
modal: true,
width: $(window).width(),
height: $(window).height()
height: $(window).height(),
open: function() {
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($query[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false }
});
codeEditor.setOption("mode", "text/x-sqlite");
CodeMirror.autoLoadMode(codeEditor, "sql");
codeEditor.focus();
}
});
}
async function execute() {
const sqlQuery = queryEl.val();
const sqlQuery = codeEditor.getValue();
const result = await server.post("sql/execute", {
query: sqlQuery
@@ -34,8 +54,8 @@ const sqlConsole = (function() {
const rows = result.rows;
resultHeadEl.empty();
resultBodyEl.empty();
$resultHead.empty();
$resultBody.empty();
if (rows.length > 0) {
const result = rows[0];
@@ -45,7 +65,7 @@ const sqlConsole = (function() {
rowEl.append($("<th>").html(key));
}
resultHeadEl.append(rowEl);
$resultHead.append(rowEl);
}
for (const result of rows) {
@@ -55,15 +75,15 @@ const sqlConsole = (function() {
rowEl.append($("<td>").html(result[key]));
}
resultBodyEl.append(rowEl);
$resultBody.append(rowEl);
}
}
$(document).bind('keydown', 'alt+o', showDialog);
queryEl.bind('keydown', 'ctrl+return', execute);
$query.bind('keydown', 'ctrl+return', execute);
executeButton.click(execute);
$executeButton.click(execute);
return {
showDialog

View File

@@ -126,6 +126,10 @@ $.ui.autocomplete.filter = (array, terms) => {
if (found) {
results.push(item);
if (results.length > 100) {
break;
}
}
}
@@ -192,4 +196,21 @@ $(document).ready(() => {
executeScript(script);
}
});
});
});
if (isElectron()) {
require('electron').ipcRenderer.on('create-day-sub-note', async function(event, parentNoteId) {
// this might occur when day note had to be created
if (!noteTree.noteExists(parentNoteId)) {
await noteTree.reload();
}
await noteTree.activateNode(parentNoteId);
setTimeout(() => {
const node = noteTree.getCurrentNode();
noteTree.createNote(node, node.data.noteId, 'into', node.data.isProtected);
}, 500);
});
}

View File

@@ -116,6 +116,32 @@ const noteEditor = (function() {
isNewNoteCreated = true;
}
function setContent(content) {
if (currentNote.detail.type === 'text') {
// temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
editor.setData(content ? content : "<p></p>");
noteDetailEl.show();
noteDetailCodeEl.hide();
noteDetailRenderEl.html('').hide();
}
else if (currentNote.detail.type === 'code') {
noteDetailEl.hide();
noteDetailCodeEl.show();
noteDetailRenderEl.html('').hide();
// this needs to happen after the element is shown, otherwise the editor won't be refresheds
codeEditor.setValue(content);
const info = CodeMirror.findModeByMIME(currentNote.detail.mime);
if (info) {
codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(codeEditor, info.mode);
}
}
}
async function loadNoteToEditor(noteId) {
currentNote = await loadNote(noteId);
@@ -146,30 +172,7 @@ const noteEditor = (function() {
noteType.setNoteType(currentNote.detail.type);
noteType.setNoteMime(currentNote.detail.mime);
if (currentNote.detail.type === 'text') {
// temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
editor.setData(currentNote.detail.content ? currentNote.detail.content : "<p></p>");
noteDetailEl.show();
noteDetailCodeEl.hide();
noteDetailRenderEl.html('').hide();
}
else if (currentNote.detail.type === 'code') {
noteDetailEl.hide();
noteDetailCodeEl.show();
noteDetailRenderEl.html('').hide();
// this needs to happen after the element is shown, otherwise the editor won't be refresheds
codeEditor.setValue(currentNote.detail.content);
const info = CodeMirror.findModeByMIME(currentNote.detail.mime);
if (info) {
codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(codeEditor, info.mode);
}
}
else if (currentNote.detail.type === 'render') {
if (currentNote.detail.type === 'render') {
noteDetailEl.hide();
noteDetailCodeEl.hide();
noteDetailRenderEl.html('').show();
@@ -179,7 +182,7 @@ const noteEditor = (function() {
noteDetailRenderEl.html(subTree);
}
else {
throwError("Unrecognized type " + currentNote.detail.type);
setContent(currentNote.detail.content);
}
noteChangeDisabled = false;
@@ -314,6 +317,7 @@ const noteEditor = (function() {
getEditor,
focus,
executeCurrentNote,
loadAttributeList
loadAttributeList,
setContent
};
})();

View File

@@ -14,6 +14,8 @@ const noteTree = (function() {
let parentChildToNoteTreeId = {};
let noteIdToTitle = {};
let hiddenInAutocomplete = {};
function getNoteTreeId(parentNoteId, childNoteId) {
assertArguments(parentNoteId, childNoteId);
@@ -640,16 +642,21 @@ const noteTree = (function() {
return document.location.hash.substr(1); // strip initial #
}
function loadTree() {
return server.get('tree').then(resp => {
startNotePath = resp.start_note_path;
async function loadTree() {
const resp = await server.get('tree');
startNotePath = resp.start_note_path;
if (document.location.hash) {
startNotePath = getNotePathFromAddress();
}
if (document.location.hash) {
startNotePath = getNotePathFromAddress();
}
return prepareNoteTree(resp.notes);
});
hiddenInAutocomplete = {};
for (const noteId of resp.hiddenInAutocomplete) {
hiddenInAutocomplete[noteId] = true;
}
return prepareNoteTree(resp.notes);
}
$(() => loadTree().then(noteTree => initFancyTree(noteTree)));
@@ -706,6 +713,10 @@ const noteTree = (function() {
const autocompleteItems = [];
for (const childNoteId of parentToChildren[parentNoteId]) {
if (hiddenInAutocomplete[childNoteId]) {
continue;
}
const childNotePath = (notePath ? (notePath + '/') : '') + childNoteId;
const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + getNoteTitle(childNoteId, parentNoteId);
@@ -775,7 +786,7 @@ const noteTree = (function() {
};
if (target === 'after') {
node.appendSibling(newNode).setActive(true);
await node.appendSibling(newNode).setActive(true);
}
else if (target === 'into') {
if (!node.getChildren() && node.isFolder()) {
@@ -785,7 +796,7 @@ const noteTree = (function() {
node.addChildren(newNode);
}
node.getLastChild().setActive(true);
await node.getLastChild().setActive(true);
node.folder = true;
node.renderTitle();
@@ -794,6 +805,8 @@ const noteTree = (function() {
throwError("Unrecognized target: " + target);
}
clearSelectedNodes(); // to unmark previously active node
showMessage("Created!");
}
@@ -803,6 +816,10 @@ const noteTree = (function() {
await reload();
}
function noteExists(noteId) {
return !!childToParents[noteId];
}
$(document).bind('keydown', 'ctrl+o', e => {
const node = getCurrentNode();
const parentNoteId = node.data.parentNoteId;
@@ -876,6 +893,7 @@ const noteTree = (function() {
removeParentChildRelation,
setParentChildRelation,
getSelectedNodes,
sortAlphabetically
sortAlphabetically,
noteExists
};
})();

View File

@@ -104,6 +104,8 @@ const server = (function() {
post,
put,
remove,
exec
exec,
// don't remove, used from CKEditor image upload!
getHeaders
}
})();

File diff suppressed because one or more lines are too long

View File

@@ -12,7 +12,7 @@ const attributes = require('../../services/attributes');
router.get('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
const noteId = req.params.noteId;
res.send(await sql.getRows("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId]));
res.send(await sql.getRows("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId]));
}));
router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
@@ -23,19 +23,26 @@ router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res,
await sql.doInTransaction(async () => {
for (const attr of attributes) {
if (attr.attributeId) {
await sql.execute("UPDATE attributes SET name = ?, value = ?, dateModified = ? WHERE attributeId = ?",
[attr.name, attr.value, now, attr.attributeId]);
await sql.execute("UPDATE attributes SET name = ?, value = ?, dateModified = ?, isDeleted = ?, position = ? WHERE attributeId = ?",
[attr.name, attr.value, now, attr.isDeleted, attr.position, attr.attributeId]);
}
else {
// if it was "created" and then immediatelly deleted, we just don't create it at all
if (attr.isDeleted) {
continue;
}
attr.attributeId = utils.newAttributeId();
await sql.insert("attributes", {
attributeId: attr.attributeId,
noteId: noteId,
name: attr.name,
value: attr.value,
dateCreated: now,
dateModified: now
attributeId: attr.attributeId,
noteId: noteId,
name: attr.name,
value: attr.value,
position: attr.position,
dateCreated: now,
dateModified: now,
isDeleted: false
});
}
@@ -43,11 +50,11 @@ router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res,
}
});
res.send(await sql.getRows("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId]));
res.send(await sql.getRows("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId]));
}));
router.get('/attributes/names', auth.checkApiAuth, wrap(async (req, res, next) => {
const names = await sql.getColumn("SELECT DISTINCT name FROM attributes");
const names = await sql.getColumn("SELECT DISTINCT name FROM attributes WHERE isDeleted = 0");
for (const attr of attributes.BUILTIN_ATTRIBUTES) {
if (!names.includes(attr)) {
@@ -63,7 +70,7 @@ router.get('/attributes/names', auth.checkApiAuth, wrap(async (req, res, next) =
router.get('/attributes/values/:attributeName', auth.checkApiAuth, wrap(async (req, res, next) => {
const attributeName = req.params.attributeName;
const values = await sql.getColumn("SELECT DISTINCT value FROM attributes WHERE name = ? AND value != '' ORDER BY value", [attributeName]);
const values = await sql.getColumn("SELECT DISTINCT value FROM attributes WHERE isDeleted = 0 AND name = ? AND value != '' ORDER BY value", [attributeName]);
res.send(values);
}));

View File

@@ -4,16 +4,8 @@ const express = require('express');
const router = express.Router();
const sql = require('../../services/sql');
const auth = require('../../services/auth');
const utils = require('../../services/utils');
const sync_table = require('../../services/sync_table');
const image = require('../../services/image');
const multer = require('multer')();
const imagemin = require('imagemin');
const imageminMozJpeg = require('imagemin-mozjpeg');
const imageminPngQuant = require('imagemin-pngquant');
const imageminGifLossy = require('imagemin-giflossy');
const jimp = require('jimp');
const imageType = require('image-type');
const sanitizeFilename = require('sanitize-filename');
const wrap = require('express-promise-wrap').wrap;
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
const fs = require('fs');
@@ -49,45 +41,7 @@ router.post('', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async
return res.status(400).send("Unknown image type: " + file.mimetype);
}
const now = utils.nowDate();
const resizedImage = await resize(file.buffer);
const optimizedImage = await optimize(resizedImage);
const imageFormat = imageType(optimizedImage);
const fileNameWithouExtension = file.originalname.replace(/\.[^/.]+$/, "");
const fileName = sanitizeFilename(fileNameWithouExtension + "." + imageFormat.ext);
const imageId = utils.newImageId();
await sql.doInTransaction(async () => {
await sql.insert("images", {
imageId: imageId,
format: imageFormat.ext,
name: fileName,
checksum: utils.hash(optimizedImage),
data: optimizedImage,
isDeleted: 0,
dateModified: now,
dateCreated: now
});
await sync_table.addImageSync(imageId, sourceId);
const noteImageId = utils.newNoteImageId();
await sql.insert("note_images", {
noteImageId: noteImageId,
noteId: noteId,
imageId: imageId,
isDeleted: 0,
dateModified: now,
dateCreated: now
});
await sync_table.addNoteImageSync(noteImageId, sourceId);
});
const {fileName, imageId} = await image.saveImage(file, sourceId, noteId);
res.send({
uploaded: true,
@@ -95,54 +49,4 @@ router.post('', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async
});
}));
const MAX_SIZE = 1000;
const MAX_BYTE_SIZE = 200000; // images should have under 100 KBs
async function resize(buffer) {
const image = await jimp.read(buffer);
if (image.bitmap.width > image.bitmap.height && image.bitmap.width > MAX_SIZE) {
image.resize(MAX_SIZE, jimp.AUTO);
}
else if (image.bitmap.height > MAX_SIZE) {
image.resize(jimp.AUTO, MAX_SIZE);
}
else if (buffer.byteLength <= MAX_BYTE_SIZE) {
return buffer;
}
// we do resizing with max quality which will be trimmed during optimization step next
image.quality(100);
// when converting PNG to JPG we lose alpha channel, this is replaced by white to match Trilium white background
image.background(0xFFFFFFFF);
// getBuffer doesn't support promises so this workaround
return await new Promise((resolve, reject) => image.getBuffer(jimp.MIME_JPEG, (err, data) => {
if (err) {
reject(err);
}
else {
resolve(data);
}
}));
}
async function optimize(buffer) {
return await imagemin.buffer(buffer, {
plugins: [
imageminMozJpeg({
quality: 50
}),
imageminPngQuant({
quality: "0-70"
}),
imageminGifLossy({
lossy: 80,
optimize: '3' // needs to be string
})
]
});
}
module.exports = router;

View File

@@ -66,7 +66,7 @@ async function importNotes(dir, parentNoteId) {
const noteText = fs.readFileSync(path, "utf8");
const noteId = utils.newNoteId();
const noteTreeId = utils.newnoteRevisionId();
const noteTreeId = utils.newNoteRevisionId();
const now = utils.nowDate();

View File

@@ -14,7 +14,7 @@ const wrap = require('express-promise-wrap').wrap;
router.post('/sync', wrap(async (req, res, next) => {
const timestampStr = req.body.timestamp;
const timestamp = utils.parseDate(timestampStr);
const timestamp = utils.parseDateTime(timestampStr);
const now = new Date();

View File

@@ -62,6 +62,8 @@ router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
const {query, params} = getSearchQuery(attrFilters, searchText);
console.log(query, params);
const noteIds = await sql.getColumn(query, params);
res.send(noteIds);
@@ -152,7 +154,7 @@ function getSearchQuery(attrFilters, searchText) {
searchParams.push(searchText); // two occurences in searchCondition
}
const query = `SELECT notes.noteId FROM notes
const query = `SELECT DISTINCT notes.noteId FROM notes
${joins.join('\r\n')}
WHERE
notes.isDeleted = 0

View File

@@ -45,7 +45,8 @@ async function getRecentNotes() {
recent_notes.isDeleted = 0
AND note_tree.isDeleted = 0
ORDER BY
dateAccessed DESC`);
dateAccessed DESC
LIMIT 200`);
}
module.exports = router;

106
src/routes/api/sender.js Normal file
View File

@@ -0,0 +1,106 @@
"use strict";
const express = require('express');
const router = express.Router();
const image = require('../../services/image');
const utils = require('../../services/utils');
const date_notes = require('../../services/date_notes');
const sql = require('../../services/sql');
const wrap = require('express-promise-wrap').wrap;
const notes = require('../../services/notes');
const multer = require('multer')();
const password_encryption = require('../../services/password_encryption');
const options = require('../../services/options');
const sync_table = require('../../services/sync_table');
router.post('/login', wrap(async (req, res, next) => {
const username = req.body.username;
const password = req.body.password;
const isUsernameValid = username === await options.getOption('username');
const isPasswordValid = await password_encryption.verifyPassword(password);
if (!isUsernameValid || !isPasswordValid) {
res.status(401).send("Incorrect username/password");
}
else {
const token = utils.randomSecureToken();
await sql.doInTransaction(async () => {
const apiTokenId = utils.newApiTokenId();
await sql.insert("api_tokens", {
apiTokenId: apiTokenId,
token: token,
dateCreated: utils.nowDate(),
isDeleted: false
});
await sync_table.addApiTokenSync(apiTokenId);
});
res.send({
token: token
});
}
}));
async function checkSenderToken(req, res, next) {
const token = req.headers.authorization;
if (await sql.getValue("SELECT COUNT(*) FROM api_tokens WHERE isDeleted = 0 AND token = ?", [token]) === 0) {
res.status(401).send("Not authorized");
}
else if (await sql.isDbUpToDate()) {
next();
}
else {
res.status(409).send("Mismatched app versions"); // need better response than that
}
}
router.post('/image', checkSenderToken, multer.single('upload'), wrap(async (req, res, next) => {
const file = req.file;
if (!["image/png", "image/jpeg", "image/gif"].includes(file.mimetype)) {
return res.status(400).send("Unknown image type: " + file.mimetype);
}
const parentNoteId = await date_notes.getDateNoteId(req.headers['x-local-date']);
const noteId = (await notes.createNewNote(parentNoteId, {
title: "Sender image",
content: "",
target: 'into',
isProtected: false,
type: 'text',
mime: 'text/html'
})).noteId;
const {fileName, imageId} = await image.saveImage(file, null, noteId);
const url = `/api/images/${imageId}/${fileName}`;
const content = `<img src="${url}"/>`;
await sql.execute("UPDATE notes SET content = ? WHERE noteId = ?", [content, noteId]);
res.send({});
}));
router.post('/note', checkSenderToken, wrap(async (req, res, next) => {
const parentNoteId = await date_notes.getDateNoteId(req.headers['x-local-date']);
await notes.createNewNote(parentNoteId, {
title: req.body.title,
content: req.body.content,
target: 'into',
isProtected: false,
type: 'text',
mime: 'text/html'
});
res.send({});
}));
module.exports = router;

View File

@@ -147,6 +147,12 @@ router.get('/attributes/:attributeId', auth.checkApiAuth, wrap(async (req, res,
res.send(await sql.getRow("SELECT * FROM attributes WHERE attributeId = ?", [attributeId]));
}));
router.get('/api_tokens/:apiTokenId', auth.checkApiAuth, wrap(async (req, res, next) => {
const apiTokenId = req.params.apiTokenId;
res.send(await sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [apiTokenId]));
}));
router.put('/notes', auth.checkApiAuth, wrap(async (req, res, next) => {
await syncUpdate.updateNote(req.body.entity, req.body.sourceId);
@@ -201,4 +207,10 @@ router.put('/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
res.send({});
}));
router.put('/api_tokens', auth.checkApiAuth, wrap(async (req, res, next) => {
await syncUpdate.updateApiToken(req.body.entity, req.body.sourceId);
res.send({});
}));
module.exports = router;

View File

@@ -29,8 +29,20 @@ router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
protected_session.decryptNotes(req, notes);
const hiddenInAutocomplete = await sql.getColumn(`
SELECT
DISTINCT noteId
FROM
attributes
JOIN notes USING(noteId)
WHERE
attributes.name = 'hide_in_autocomplete'
AND attributes.isDeleted = 0
AND notes.isDeleted = 0`);
res.send({
notes: notes,
hiddenInAutocomplete: hiddenInAutocomplete,
start_note_path: await options.getOption('start_note_path')
});
}));

View File

@@ -61,10 +61,8 @@ router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, wrap
await sync_table.addNoteReorderingSync(beforeNote.parentNoteId, sourceId);
const now = utils.nowDate();
await sql.execute("UPDATE note_tree SET parentNoteId = ?, notePosition = ?, dateModified = ? WHERE noteTreeId = ?",
[beforeNote.parentNoteId, beforeNote.notePosition, now, noteTreeId]);
[beforeNote.parentNoteId, beforeNote.notePosition, utils.nowDate(), noteTreeId]);
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
});

View File

@@ -28,6 +28,7 @@ const cleanupRoute = require('./api/cleanup');
const imageRoute = require('./api/image');
const attributesRoute = require('./api/attributes');
const scriptRoute = require('./api/script');
const senderRoute = require('./api/sender');
function register(app) {
app.use('/', indexRoute);
@@ -59,6 +60,7 @@ function register(app) {
app.use('/api/cleanup', cleanupRoute);
app.use('/api/images', imageRoute);
app.use('/api/script', scriptRoute);
app.use('/api/sender', senderRoute);
}
module.exports = {

View File

@@ -8,4 +8,6 @@ window.goToday = async function() {
});
api.activateNote(todayNoteId);
};
};
$(document).bind('keydown', "alt+t", window.goToday);

View File

@@ -40,7 +40,8 @@
await this.createNote(parentNoteId, 'data', jsonContent, {
json: true,
attributes: {
date_data: date
date_data: date,
hide_in_autocomplete: null
}
});
}
@@ -70,7 +71,9 @@
return data;
});
var config = {
const ctx = $("#canvas")[0].getContext("2d");
new Chart(ctx, {
type: 'line',
data: {
labels: data.map(row => row.date),
@@ -82,10 +85,7 @@
fill: false
}]
}
};
var ctx = $("#canvas")[0].getContext("2d");
new Chart(ctx, config);
});
}
$("#weight-form").submit(event => {

View File

@@ -3,7 +3,7 @@
const build = require('./build');
const packageJson = require('../../package');
const APP_DB_VERSION = 71;
const APP_DB_VERSION = 76;
module.exports = {
app_version: packageJson.version,

View File

@@ -5,7 +5,12 @@ const utils = require('./utils');
const sync_table = require('./sync_table');
const Repository = require('./repository');
const BUILTIN_ATTRIBUTES = [ 'run_on_startup', 'disable_versioning' ];
const BUILTIN_ATTRIBUTES = [
'run_on_startup',
'disable_versioning',
'calendar_root',
'hide_in_autocomplete'
];
async function getNoteAttributeMap(noteId) {
return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ?`, [noteId]);
@@ -13,7 +18,10 @@ async function getNoteAttributeMap(noteId) {
async function getNoteIdWithAttribute(name, value) {
return await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId)
WHERE notes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]);
WHERE notes.isDeleted = 0
AND attributes.isDeleted = 0
AND attributes.name = ?
AND attributes.value = ?`, [name, value]);
}
async function getNotesWithAttribute(dataKey, name, value) {
@@ -23,11 +31,11 @@ async function getNotesWithAttribute(dataKey, name, value) {
if (value !== undefined) {
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)
WHERE notes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]);
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]);
}
else {
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)
WHERE notes.isDeleted = 0 AND attributes.name = ?`, [name]);
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ?`, [name]);
}
return notes;
@@ -41,7 +49,7 @@ async function getNoteWithAttribute(dataKey, name, value) {
async function getNoteIdsWithAttribute(name) {
return await sql.getColumn(`SELECT DISTINCT notes.noteId FROM notes JOIN attributes USING(noteId)
WHERE notes.isDeleted = 0 AND attributes.name = ?`, [name]);
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.isDeleted = 0`, [name]);
}
async function createAttribute(noteId, name, value = null, sourceId = null) {
@@ -54,7 +62,8 @@ async function createAttribute(noteId, name, value = null, sourceId = null) {
name: name,
value: value,
dateModified: now,
dateCreated: now
dateCreated: now,
isDeleted: false
});
await sync_table.addAttributeSync(attributeId, sourceId);

View File

@@ -10,7 +10,7 @@ const sync_mutex = require('./sync_mutex');
async function regularBackup() {
const now = new Date();
const lastBackupDate = utils.parseDate(await options.getOption('last_backup_date'));
const lastBackupDate = utils.parseDateTime(await options.getOption('last_backup_date'));
console.log(lastBackupDate);

View File

@@ -223,6 +223,8 @@ async function runAllChecks() {
await runSyncRowChecks("recent_notes", "noteTreeId", errorList);
await runSyncRowChecks("images", "imageId", errorList);
await runSyncRowChecks("note_images", "noteImageId", errorList);
await runSyncRowChecks("attributes", "attributeId", errorList);
await runSyncRowChecks("api_tokens", "apiTokenId", errorList);
if (errorList.length === 0) {
// we run this only if basic checks passed since this assumes basic data consistency

View File

@@ -3,12 +3,16 @@
const sql = require('./sql');
const notes = require('./notes');
const attributes = require('./attributes');
const utils = require('./utils');
const CALENDAR_ROOT_ATTRIBUTE = 'calendar_root';
const YEAR_ATTRIBUTE = 'year_note';
const MONTH_ATTRIBUTE = 'month_note';
const DATE_ATTRIBUTE = 'date_note';
const DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
async function createNote(parentNoteId, noteTitle, noteText) {
return (await notes.createNewNote(parentNoteId, {
title: noteTitle,
@@ -72,7 +76,11 @@ async function getMonthNoteId(dateTimeStr, rootNoteId) {
monthNoteId = await getNoteStartingWith(yearNoteId, monthNumber);
if (!monthNoteId) {
monthNoteId = await createNote(yearNoteId, monthNumber);
const dateObj = utils.parseDate(dateTimeStr);
const noteTitle = monthNumber + " - " + MONTHS[dateObj.getMonth()];
monthNoteId = await createNote(yearNoteId, noteTitle);
}
await attributes.createAttribute(monthNoteId, MONTH_ATTRIBUTE, monthStr);
@@ -97,7 +105,11 @@ async function getDateNoteId(dateTimeStr, rootNoteId = null) {
dateNoteId = await getNoteStartingWith(monthNoteId, dayNumber);
if (!dateNoteId) {
dateNoteId = await createNote(monthNoteId, dayNumber);
const dateObj = utils.parseDate(dateTimeStr);
const noteTitle = dayNumber + " - " + DAYS[dateObj.getDay()];
dateNoteId = await createNote(monthNoteId, noteTitle);
}
await attributes.createAttribute(dateNoteId, DATE_ATTRIBUTE, dateStr);

108
src/services/image.js Normal file
View File

@@ -0,0 +1,108 @@
"use strict";
const utils = require('./utils');
const sql = require('./sql');
const sync_table = require('./sync_table');
const imagemin = require('imagemin');
const imageminMozJpeg = require('imagemin-mozjpeg');
const imageminPngQuant = require('imagemin-pngquant');
const imageminGifLossy = require('imagemin-giflossy');
const jimp = require('jimp');
const imageType = require('image-type');
const sanitizeFilename = require('sanitize-filename');
async function saveImage(file, sourceId, noteId) {
const resizedImage = await resize(file.buffer);
const optimizedImage = await optimize(resizedImage);
const imageFormat = imageType(optimizedImage);
const fileNameWithouExtension = file.originalname.replace(/\.[^/.]+$/, "");
const fileName = sanitizeFilename(fileNameWithouExtension + "." + imageFormat.ext);
const imageId = utils.newImageId();
const now = utils.nowDate();
await sql.doInTransaction(async () => {
await sql.insert("images", {
imageId: imageId,
format: imageFormat.ext,
name: fileName,
checksum: utils.hash(optimizedImage),
data: optimizedImage,
isDeleted: 0,
dateModified: now,
dateCreated: now
});
await sync_table.addImageSync(imageId, sourceId);
const noteImageId = utils.newNoteImageId();
await sql.insert("note_images", {
noteImageId: noteImageId,
noteId: noteId,
imageId: imageId,
isDeleted: 0,
dateModified: now,
dateCreated: now
});
await sync_table.addNoteImageSync(noteImageId, sourceId);
});
return {fileName, imageId};
}
const MAX_SIZE = 1000;
const MAX_BYTE_SIZE = 200000; // images should have under 100 KBs
async function resize(buffer) {
const image = await jimp.read(buffer);
if (image.bitmap.width > image.bitmap.height && image.bitmap.width > MAX_SIZE) {
image.resize(MAX_SIZE, jimp.AUTO);
}
else if (image.bitmap.height > MAX_SIZE) {
image.resize(jimp.AUTO, MAX_SIZE);
}
else if (buffer.byteLength <= MAX_BYTE_SIZE) {
return buffer;
}
// we do resizing with max quality which will be trimmed during optimization step next
image.quality(100);
// when converting PNG to JPG we lose alpha channel, this is replaced by white to match Trilium white background
image.background(0xFFFFFFFF);
// getBuffer doesn't support promises so this workaround
return await new Promise((resolve, reject) => image.getBuffer(jimp.MIME_JPEG, (err, data) => {
if (err) {
reject(err);
}
else {
resolve(data);
}
}));
}
async function optimize(buffer) {
return await imagemin.buffer(buffer, {
plugins: [
imageminMozJpeg({
quality: 50
}),
imageminPngQuant({
quality: "0-70"
}),
imageminGifLossy({
lossy: 80,
optimize: '3' // needs to be string
})
]
});
}
module.exports = {
saveImage
};

View File

@@ -22,7 +22,7 @@ function info(message) {
function error(message) {
// we're using .info() instead of .error() because simple-node-logger emits weird error for showError()
info(message);
info("ERROR: " + message);
}
const requestBlacklist = [ "/libraries", "/javascripts", "/images", "/stylesheets" ];

View File

@@ -154,10 +154,10 @@ async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) {
note.isProtected = false;
}
const newnoteRevisionId = utils.newnoteRevisionId();
const newNoteRevisionId = utils.newNoteRevisionId();
await sql.insert('note_revisions', {
noteRevisionId: newnoteRevisionId,
noteRevisionId: newNoteRevisionId,
noteId: noteId,
// title and text should be decrypted now
title: oldNote.title,
@@ -167,7 +167,7 @@ async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) {
dateModifiedTo: nowStr
});
await sync_table.addNoteHistorySync(newnoteRevisionId, sourceId);
await sync_table.addNoteHistorySync(newNoteRevisionId, sourceId);
}
async function saveNoteImages(noteId, noteText, sourceId) {
@@ -235,7 +235,7 @@ async function updateNote(noteId, newNote, dataKey, sourceId) {
"SELECT noteRevisionId FROM note_revisions WHERE noteId = ? AND dateModifiedTo >= ?", [noteId, historyCutoff]);
await sql.doInTransaction(async () => {
const msSinceDateCreated = now.getTime() - utils.parseDate(newNote.detail.dateCreated).getTime();
const msSinceDateCreated = now.getTime() - utils.parseDateTime(newNote.detail.dateCreated).getTime();
if (attributesMap.disable_versioning !== 'true'
&& !existingnoteRevisionId

View File

@@ -54,6 +54,8 @@ function ScriptContext(noteId, dataKey) {
return noteId;
};
this.createAttribute = attributes.createAttribute;
this.updateEntity = this.repository.updateEntity;
this.log = function(message) {

View File

@@ -149,6 +149,9 @@ async function pullSync(syncContext) {
else if (sync.entityName === 'attributes') {
await syncUpdate.updateAttribute(resp, syncContext.sourceId);
}
else if (sync.entityName === 'api_tokens') {
await syncUpdate.updateApiToken(resp, syncContext.sourceId);
}
else {
throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`);
}
@@ -233,6 +236,9 @@ async function pushEntity(sync, syncContext) {
else if (sync.entityName === 'attributes') {
entity = await sql.getRow('SELECT * FROM attributes WHERE attributeId = ?', [sync.entityId]);
}
else if (sync.entityName === 'api_tokens') {
entity = await sql.getRow('SELECT * FROM api_tokens WHERE apiTokenId = ?', [sync.entityId]);
}
else {
throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`);
}

View File

@@ -40,6 +40,10 @@ async function addAttributeSync(attributeId, sourceId) {
await addEntitySync("attributes", attributeId, sourceId);
}
async function addApiTokenSync(apiTokenId, sourceId) {
await addEntitySync("api_tokens", apiTokenId, sourceId);
}
async function addEntitySync(entityName, entityId, sourceId) {
await sql.replace("sync", {
entityName: entityName,
@@ -93,6 +97,7 @@ async function fillAllSyncRows() {
await fillSyncRows("images", "imageId");
await fillSyncRows("note_images", "noteImageId");
await fillSyncRows("attributes", "attributeId");
await fillSyncRows("api_tokens", "apiTokenId");
}
module.exports = {
@@ -105,6 +110,7 @@ module.exports = {
addImageSync,
addNoteImageSync,
addAttributeSync,
addApiTokenSync,
addEntitySync,
cleanupSyncRowsForMissingEntities,
fillAllSyncRows

View File

@@ -137,6 +137,20 @@ async function updateAttribute(entity, sourceId) {
}
}
async function updateApiToken(entity, sourceId) {
const apiTokenId = await sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [entity.apiTokenId]);
if (!apiTokenId) {
await sql.doInTransaction(async () => {
await sql.replace("api_tokens", entity);
await sync_table.addApiTokenSync(entity.apiTokenId, sourceId);
});
log.info("Update/sync API token " + entity.apiTokenId);
}
}
module.exports = {
updateNote,
updateNoteTree,
@@ -146,5 +160,6 @@ module.exports = {
updateRecentNotes,
updateImage,
updateNoteImage,
updateAttribute
updateAttribute,
updateApiToken
};

View File

@@ -2,6 +2,7 @@
const sql = require('./sql');
const sync_table = require('./sync_table');
const protected_session = require('./protected_session');
async function validateParentChild(res, parentNoteId, childNoteId, noteTreeId = null) {
const existing = await getExistingNoteTree(parentNoteId, childNoteId);

View File

@@ -11,7 +11,7 @@ function newNoteTreeId() {
return randomString(12);
}
function newnoteRevisionId() {
function newNoteRevisionId() {
return randomString(12);
}
@@ -27,6 +27,10 @@ function newAttributeId() {
return randomString(12);
}
function newApiTokenId() {
return randomString(12);
}
function randomString(length) {
return randtoken.generate(length);
}
@@ -39,6 +43,14 @@ function nowDate() {
return dateStr(new Date());
}
function localDate() {
const date = new Date();
return date.getFullYear() + "-"
+ (date.getMonth() < 9 ? "0" : "") + (date.getMonth() + 1) + "-"
+ (date.getDate() < 10 ? "0" : "") + date.getDate();
}
function dateStr(date) {
return date.toISOString();
}
@@ -47,7 +59,7 @@ function dateStr(date) {
* @param str - needs to be in the ISO 8601 format "YYYY-MM-DDTHH:MM:SS.sssZ" format as outputted by dateStr().
* also is assumed to be GMT time (as indicated by the "Z" at the end), *not* local time
*/
function parseDate(str) {
function parseDateTime(str) {
try {
return new Date(Date.parse(str));
}
@@ -56,6 +68,12 @@ function parseDate(str) {
}
}
function parseDate(str) {
const datePart = str.substr(0, 10);
return parseDateTime(datePart + "T12:00:00.000Z");
}
function toBase64(plainText) {
return Buffer.from(plainText).toString('base64');
}
@@ -115,14 +133,17 @@ module.exports = {
randomSecureToken,
randomString,
nowDate,
localDate,
dateStr,
parseDate,
parseDateTime,
newNoteId,
newNoteTreeId,
newnoteRevisionId,
newNoteRevisionId,
newImageId,
newNoteImageId,
newAttributeId,
newApiTokenId,
toBase64,
fromBase64,
hmac,

View File

@@ -151,20 +151,7 @@
</div>
<div id="recent-notes-dialog" title="Recent notes" style="display: none;">
<select id="recent-notes-select-box" size="20" style="width: 100%">
</select>
<br/><br/>
<p>
<button class="btn btn-sm" id="recent-notes-jump-to">Jump to <kbd>enter</kbd></button>
&nbsp;
<button class="btn btn-sm" id="recent-notes-add-link">Add link <kbd>l</kbd></button>
<button class="btn btn-sm" id="recent-notes-add-current-as-child">Add current as child <kbd>c</kbd></button>
<button class="btn btn-sm" id="recent-notes-add-recent-as-child">Add recent as child <kbd>r</kbd></button>
</p>
<input id="recent-notes-search-input" class="form-control"/>
</div>
<div id="add-link-dialog" title="Add link" style="display: none;">
@@ -373,8 +360,11 @@
</div>
<div id="sql-console-dialog" title="SQL console" style="display: none; padding: 20px;">
<textarea style="width: 100%; height: 100px" id="sql-console-query"></textarea>
<button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button>
<div style="height: 150px; width: 100%; border: 1px solid #ccc; margin-bottom: 10px;" id="sql-console-query"></div>
<div style="text-align: center">
<button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button>
</div>
<table id="sql-console-results" class="table table-striped" style="overflow: scroll; width: 100%;">
<thead></thead>
@@ -389,31 +379,40 @@
<div id="attributes-dialog" title="Note attributes" style="display: none; padding: 20px;">
<form data-bind="submit: save">
<div style="text-align: center">
<button class="btn btn-large" style="width: 200px;" id="save-attributes-button" type="submit">Save <kbd>enter</kbd></button>
<button class="btn btn-large" style="width: 200px;" id="save-attributes-button" type="submit">Save changes <kbd>enter</kbd></button>
</div>
<div style="height: 97%; overflow: auto">
<table id="attributes-table" class="table">
<thead>
<tr>
<th></th>
<th>ID</th>
<th>Name</th>
<th>Value</th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: attributes">
<tr>
<td data-bind="text: attributeId"></td>
<tr data-bind="if: isDeleted == 0">
<td class="handle">
<span class="glyphicon glyphicon-resize-vertical"></span>
<input type="hidden" name="position" data-bind="value: position"/>
</td>
<!-- ID column has specific width because if it's empty its size can be deformed when dragging -->
<td data-bind="text: attributeId" style="width: 150px;"></td>
<td>
<!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
<input type="text" class="attribute-name" data-bind="value: name, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }"/>
<div style="color: red" data-bind="if: $parent.isNotUnique($index())">Attribute name must be unique per note.</div>
<div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate attribute.</div>
<div style="color: red" data-bind="if: $parent.isEmptyName($index())">Attribute name can't be empty.</div>
</td>
<td>
<input type="text" class="attribute-value" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }" style="width: 300px"/>
</td>
<td title="Delete" style="padding: 13px;">
<span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteAttribute"></span>
</td>
</tr>
</tbody>
</table>