Compare commits

...

58 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
85d32c66f2 release 0.5.6 2018-02-06 00:06:04 -05:00
azivner
4e70cebf70 recent notes now use autocomplete instead of select box, closes #36 2018-02-05 23:50:25 -05:00
azivner
214d2e7659 correct quoting rules for attribute/status bar 2018-02-05 22:28:12 -05:00
azivner
f380bb7f65 removal of extra console logs 2018-02-05 22:26:50 -05:00
azivner
0a9a032daa fix incorrect removal of attribute filter from string, fixes #35 2018-02-05 22:25:25 -05:00
azivner
23a2b58b24 fix #32, could not open attribute dialog if it didn't have any attributes yet 2018-02-05 21:07:18 -05:00
azivner
aee64b2522 fix visual glitch in search - showing search now doesn't move note content 2018-02-05 20:53:04 -05:00
azivner
02e07ec03a release 0.5.5-beta 2018-02-04 23:19:20 -05:00
azivner
3d2dc8e699 fixes for change propagation (conflict between knockout and jquery UI autocomplete) 2018-02-04 23:16:45 -05:00
azivner
c84e15c9be implemented query language for attributes, closes #26 2018-02-04 22:44:15 -05:00
azivner
e18d0b9fd4 tag list in "status bar", closes #28 2018-02-04 20:23:30 -05:00
azivner
52817504d1 autocomplete for attribute values, closes #31 2018-02-04 19:43:11 -05:00
azivner
a3b31fab54 autocomplete for attribute names, issue #31 2018-02-04 19:27:27 -05:00
azivner
bc4aa3e40a removed ctrl+shift+left, ctrl+shift+right because of conflict with standard keyboard mapping, close #25 2018-02-04 18:12:17 -05:00
azivner
873ea67e9c nice UI for attributes with validation 2018-02-04 17:22:21 -05:00
azivner
2c5115003b release 0.5.4-beta 2018-02-03 13:25:29 -05:00
azivner
e8ed913374 small changes in the toolbar 2018-02-03 12:44:22 -05:00
azivner
5bffba4e2f add API to add plugin buttons, fixes 2018-02-03 10:37:57 -05:00
azivner
05575913db release 0.5.3-beta 2018-01-31 23:57:20 -05:00
azivner
31c32ff42c fixes when generating new DB 2018-01-31 23:36:39 -05:00
azivner
6a671a5c02 fix electron app icon 2018-01-31 22:39:30 -05:00
azivner
e174aec299 release 0.5.2-beta 2018-01-31 08:04:13 -05:00
azivner
d1329f60c3 fix electron build 2018-01-31 08:03:25 -05:00
63 changed files with 1328 additions and 691 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

@@ -7,15 +7,15 @@ rm -r dist/*
echo "Rebuilding binaries for linux-ia32"
./node_modules/.bin/electron-rebuild --arch=ia32
./node_modules/.bin/electron-packager src/electron --out=dist --platform=linux --arch=ia32 --overwrite
./node_modules/.bin/electron-packager . --out=dist --platform=linux --arch=ia32 --overwrite
./node_modules/.bin/electron-packager src/electron --out=dist --platform=win32 --arch=x64 --overwrite
./node_modules/.bin/electron-packager . --out=dist --platform=win32 --arch=x64 --overwrite
# we build x64 as second so that we keep X64 binaries in node_modules for local development
echo "Rebuilding binaries for linux-x64"
./node_modules/.bin/electron-rebuild --arch=x64
./node_modules/.bin/electron-packager src/electron --out=dist --platform=linux --arch=x64 --overwrite
./node_modules/.bin/electron-packager . --out=dist --platform=linux --arch=x64 --overwrite
echo "Copying required windows binaries"

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

@@ -1,119 +1,131 @@
CREATE TABLE IF NOT EXISTS "options" (
`opt_name` TEXT NOT NULL PRIMARY KEY,
`opt_value` TEXT,
`date_modified` INT
, is_synced INTEGER NOT NULL DEFAULT 0);
`name` TEXT NOT NULL PRIMARY KEY,
`value` TEXT,
`dateModified` INT,
isSynced INTEGER NOT NULL DEFAULT 0);
CREATE TABLE IF NOT EXISTS "sync" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entity_name` TEXT NOT NULL,
`entity_id` TEXT NOT NULL,
`source_id` TEXT NOT NULL,
`sync_date` TEXT NOT NULL);
CREATE UNIQUE INDEX `IDX_sync_entity_name_id` ON `sync` (
`entity_name`,
`entity_id`
);
CREATE INDEX `IDX_sync_sync_date` ON `sync` (
`sync_date`
);
CREATE TABLE `source_ids` (
`source_id` TEXT NOT NULL,
`date_created` TEXT NOT NULL,
PRIMARY KEY(`source_id`)
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entityName` TEXT NOT NULL,
`entityId` TEXT NOT NULL,
`sourceId` TEXT NOT NULL,
`syncDate` TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS "source_ids" (
`sourceId` TEXT NOT NULL,
`dateCreated` TEXT NOT NULL,
PRIMARY KEY(`sourceId`)
);
CREATE TABLE IF NOT EXISTS "notes" (
`note_id` TEXT NOT NULL,
`note_title` TEXT,
`note_text` TEXT,
`is_protected` INT NOT NULL DEFAULT 0,
`is_deleted` INT NOT NULL DEFAULT 0,
`date_created` TEXT NOT NULL,
`date_modified` TEXT NOT NULL, type TEXT NOT NULL DEFAULT 'text', mime TEXT NOT NULL DEFAULT 'text/html',
PRIMARY KEY(`note_id`)
`noteId` TEXT NOT NULL,
`title` TEXT,
`content` TEXT,
`isProtected` INT NOT NULL DEFAULT 0,
`isDeleted` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'text',
mime TEXT NOT NULL DEFAULT 'text/html',
PRIMARY KEY(`noteId`)
);
CREATE INDEX `IDX_notes_is_deleted` ON `notes` (
`is_deleted`
CREATE TABLE IF NOT EXISTS "event_log" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`noteId` TEXT,
`comment` TEXT,
`dateAdded` TEXT NOT NULL,
FOREIGN KEY(noteId) REFERENCES notes(noteId)
);
CREATE TABLE `event_log` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`note_id` TEXT,
`comment` TEXT,
`date_added` TEXT NOT NULL,
FOREIGN KEY(note_id) REFERENCES notes(note_id)
);
CREATE TABLE IF NOT EXISTS "notes_tree" (
`note_tree_id` TEXT NOT NULL,
`note_id` TEXT NOT NULL,
`parent_note_id` TEXT NOT NULL,
`note_position` INTEGER NOT NULL,
CREATE TABLE IF NOT EXISTS "note_tree" (
`noteTreeId` TEXT NOT NULL,
`noteId` TEXT NOT NULL,
`parentNoteId` TEXT NOT NULL,
`notePosition` INTEGER NOT NULL,
`prefix` TEXT,
`is_expanded` BOOLEAN,
`is_deleted` INTEGER NOT NULL DEFAULT 0,
`date_modified` TEXT NOT NULL,
PRIMARY KEY(`note_tree_id`)
`isExpanded` BOOLEAN,
`isDeleted` INTEGER NOT NULL DEFAULT 0,
`dateModified` TEXT NOT NULL,
PRIMARY KEY(`noteTreeId`)
);
CREATE INDEX `IDX_notes_tree_note_id` ON `notes_tree` (
`note_id`
CREATE TABLE IF NOT EXISTS "note_revisions" (
`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`noteId` TEXT NOT NULL,
`title` TEXT,
`content` TEXT,
`isProtected` INT NOT NULL DEFAULT 0,
`dateModifiedFrom` TEXT NOT NULL,
`dateModifiedTo` TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "notes_history" (
`note_history_id` TEXT NOT NULL PRIMARY KEY,
`note_id` TEXT NOT NULL,
`note_title` TEXT,
`note_text` TEXT,
`is_protected` INT NOT NULL DEFAULT 0,
`date_modified_from` TEXT NOT NULL,
`date_modified_to` TEXT NOT NULL
CREATE TABLE IF NOT EXISTS "recent_notes" (
`noteTreeId` TEXT NOT NULL PRIMARY KEY,
`notePath` TEXT NOT NULL,
`dateAccessed` TEXT NOT NULL,
isDeleted INT
);
CREATE INDEX `IDX_notes_history_note_id` ON `notes_history` (
`note_id`
);
CREATE INDEX `IDX_notes_history_note_date_modified_from` ON `notes_history` (
`date_modified_from`
);
CREATE INDEX `IDX_notes_history_note_date_modified_to` ON `notes_history` (
`date_modified_to`
);
CREATE TABLE `recent_notes` (
`note_tree_id` TEXT NOT NULL PRIMARY KEY,
`note_path` TEXT NOT NULL,
`date_accessed` TEXT NOT NULL,
is_deleted INT
);
CREATE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
`note_id`,
`parent_note_id`
);
CREATE TABLE images
CREATE TABLE IF NOT EXISTS "images"
(
image_id TEXT PRIMARY KEY NOT NULL,
imageId TEXT PRIMARY KEY NOT NULL,
format TEXT NOT NULL,
checksum TEXT NOT NULL,
name TEXT NOT NULL,
data BLOB,
is_deleted INT NOT NULL DEFAULT 0,
date_modified TEXT NOT NULL,
date_created TEXT NOT NULL
isDeleted INT NOT NULL DEFAULT 0,
dateModified TEXT NOT NULL,
dateCreated TEXT NOT NULL
);
CREATE TABLE notes_image
CREATE TABLE note_images
(
note_image_id TEXT PRIMARY KEY NOT NULL,
note_id TEXT NOT NULL,
image_id TEXT NOT NULL,
is_deleted INT NOT NULL DEFAULT 0,
date_modified TEXT NOT NULL,
date_created TEXT NOT NULL
noteImageId TEXT PRIMARY KEY NOT NULL,
noteId TEXT NOT NULL,
imageId TEXT NOT NULL,
isDeleted INT NOT NULL DEFAULT 0,
dateModified TEXT NOT NULL,
dateCreated TEXT NOT NULL
);
CREATE INDEX notes_image_note_id_index ON notes_image (note_id);
CREATE INDEX notes_image_image_id_index ON notes_image (image_id);
CREATE INDEX notes_image_note_id_image_id_index ON notes_image (note_id, image_id);
CREATE TABLE attributes
CREATE TABLE IF NOT EXISTS "attributes"
(
attribute_id TEXT PRIMARY KEY NOT NULL,
note_id TEXT NOT NULL,
attributeId TEXT PRIMARY KEY NOT NULL,
noteId TEXT NOT NULL,
name TEXT NOT NULL,
value TEXT,
date_created TEXT NOT NULL,
date_modified TEXT NOT NULL
position INT NOT NULL DEFAULT 0,
dateCreated TEXT NOT NULL,
dateModified TEXT NOT NULL,
isDeleted INT NOT NULL
);
CREATE INDEX attributes_note_id_index ON attributes (note_id);
CREATE UNIQUE INDEX attributes_note_id_name_index ON attributes (note_id, name);
CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
`entityName`,
`entityId`
);
CREATE INDEX `IDX_sync_syncDate` ON `sync` (
`syncDate`
);
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
`isDeleted`
);
CREATE INDEX `IDX_note_tree_noteId` ON `note_tree` (
`noteId`
);
CREATE INDEX `IDX_note_tree_noteId_parentNoteId` ON `note_tree` (
`noteId`,
`parentNoteId`
);
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (
`noteId`
);
CREATE INDEX `IDX_note_revisions_dateModifiedFrom` ON `note_revisions` (
`dateModifiedFrom`
);
CREATE INDEX `IDX_note_revisions_dateModifiedTo` ON `note_revisions` (
`dateModifiedTo`
);
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 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

@@ -2,10 +2,12 @@
const electron = require('electron');
const path = require('path');
const config = require('./services/config');
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')();
@@ -24,7 +26,7 @@ function createMainWindow() {
width: 1200,
height: 900,
title: 'Trilium Notes',
icon: path.join(__dirname, 'public/images/app-icons/png/256x256.png')
icon: path.join(__dirname, 'src/public/images/app-icons/png/256x256.png')
});
const port = config['Network']['port'] || '3000';
@@ -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");
}
});
require('./www');
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,8 +1,9 @@
{
"name": "trilium",
"description": "Trilium Notes",
"version": "0.5.1-beta",
"version": "0.6.2",
"license": "AGPL-3.0-only",
"main": "electron.js",
"repository": {
"type": "git",
"url": "https://github.com/zadam/trilium.git"
@@ -11,8 +12,8 @@
"start": "node ./bin/www",
"test-electron": "xo",
"rebuild-electron": "electron-rebuild",
"start-electron": "electron src/electron",
"build-electron": "electron-packager src/electron --out=dist --asar --overwrite --all",
"start-electron": "electron .",
"build-electron": "electron-packager . --out=dist --asar --overwrite --all",
"start-forge": "electron-forge start",
"package-forge": "electron-forge package",
"make-forge": "electron-forge make",
@@ -26,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",
@@ -59,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

@@ -0,0 +1,21 @@
const api = (function() {
const pluginButtonsEl = $("#plugin-buttons");
async function activateNote(notePath) {
await noteTree.activateNode(notePath);
}
function addButtonToToolbar(buttonId, button) {
$("#" + buttonId).remove();
button.attr('id', buttonId);
pluginButtonsEl.append(button);
}
return {
addButtonToToolbar,
activateNote
}
})();

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,12 @@
"use strict";
const attributesDialog = (function() {
const dialogEl = $("#attributes-dialog");
const $dialog = $("#attributes-dialog");
const $saveAttributesButton = $("#save-attributes-button");
const $attributesBody = $('#attributes-table tbody');
const attributesModel = new AttributesModel();
let attributeNames = [];
function AttributesModel() {
const self = this;
@@ -14,38 +18,148 @@ const attributesDialog = (function() {
const attributes = await server.get('notes/' + noteId + '/attributes');
this.attributes(attributes);
};
self.attributes(attributes.map(ko.observable));
this.addNewRow = function() {
self.attributes.push({
attributeId: '',
name: '',
value: ''
addLastEmptyRow();
attributeNames = await server.get('attributes/names');
// 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)) {
return false;
}
}
return true;
}
this.save = async 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();
if (!isValid()) {
alert("Please fix all validation errors and try saving again.");
return;
}
const noteId = noteEditor.getCurrentNoteId();
const attributes = await server.put('notes/' + noteId + '/attributes', this.attributes());
const attributesToSave = self.attributes()
.map(attr => attr())
.filter(attr => attr.attributeId !== "" || attr.name !== "");
self.attributes(attributes);
const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave);
self.attributes(attributes.map(ko.observable));
addLastEmptyRow();
showMessage("Attributes have been saved.");
noteEditor.loadAttributeList();
};
function addLastEmptyRow() {
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: '',
isDeleted: 0,
position: 0
}));
}
}
this.attributeChanged = function (data, event) {
addLastEmptyRow();
const attr = self.getTargetAttribute(event.target);
attr.valueHasMutated();
};
this.isNotUnique = function(index) {
const cur = self.attributes()[index]();
if (cur.name.trim() === "") {
return false;
}
for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) {
const attr = attrs[i]();
if (index !== i && cur.name === attr.name) {
return true;
}
}
return false;
};
this.isEmptyName = function(index) {
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;
dialogEl.dialog({
await attributesModel.loadAttributes();
$dialog.dialog({
modal: true,
width: 800,
height: 700
height: 500
});
attributesModel.loadAttributes();
}
$(document).bind('keydown', 'alt+a', e => {
@@ -56,6 +170,54 @@ const attributesDialog = (function() {
ko.applyBindings(attributesModel, document.getElementById('attributes-dialog'));
$(document).on('focus', '.attribute-name', function (e) {
if (!$(this).hasClass("ui-autocomplete-input")) {
$(this).autocomplete({
// shouldn't be required and autocomplete should just accept array of strings, but that fails
// because we have overriden filter() function in init.js
source: attributeNames.map(attr => {
return {
label: attr,
value: attr
}
}),
minLength: 0
});
}
$(this).autocomplete("search", $(this).val());
});
$(document).on('focus', '.attribute-value', async function (e) {
if (!$(this).hasClass("ui-autocomplete-input")) {
const attributeName = $(this).parent().parent().find('.attribute-name').val();
if (attributeName.trim() === "") {
return;
}
const attributeValues = await server.get('attributes/values/' + encodeURIComponent(attributeName));
if (attributeValues.length === 0) {
return;
}
$(this).autocomplete({
// shouldn't be required and autocomplete should just accept array of strings, but that fails
// because we have overriden filter() function in init.js
source: attributeValues.map(attr => {
return {
label: attr,
value: attr
}
}),
minLength: 0
});
}
$(this).autocomplete("search", $(this).val());
});
return {
showDialog
};

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

@@ -54,24 +54,6 @@ $(document).bind('keydown', 'ctrl+f', () => {
}
});
$(document).bind('keydown', "ctrl+shift+left", () => {
const node = noteTree.getCurrentNode();
node.navigate($.ui.keyCode.LEFT, true);
$("#note-detail").focus();
return false;
});
$(document).bind('keydown', "ctrl+shift+right", () => {
const node = noteTree.getCurrentNode();
node.navigate($.ui.keyCode.RIGHT, true);
$("#note-detail").focus();
return false;
});
$(document).bind('keydown', "ctrl+shift+up", () => {
const node = noteTree.getCurrentNode();
node.navigate($.ui.keyCode.UP, true);
@@ -123,7 +105,7 @@ $(window).on('beforeunload', () => {
// Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words
$.ui.autocomplete.filter = (array, terms) => {
if (!terms) {
return [];
return array;
}
const startDate = new Date();
@@ -144,6 +126,10 @@ $.ui.autocomplete.filter = (array, terms) => {
if (found) {
results.push(item);
if (results.length > 100) {
break;
}
}
}
@@ -210,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

@@ -9,6 +9,8 @@ const noteEditor = (function() {
const unprotectButton = $("#unprotect-button");
const noteDetailWrapperEl = $("#note-detail-wrapper");
const noteIdDisplayEl = $("#note-id-display");
const attributeListEl = $("#attribute-list");
const attributeListInnerEl = $("#attribute-list-inner");
let editor = null;
let codeEditor = null;
@@ -114,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);
@@ -144,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();
@@ -177,7 +182,7 @@ const noteEditor = (function() {
noteDetailRenderEl.html(subTree);
}
else {
throwError("Unrecognized type " + currentNote.detail.type);
setContent(currentNote.detail.content);
}
noteChangeDisabled = false;
@@ -187,6 +192,27 @@ const noteEditor = (function() {
// after loading new note make sure editor is scrolled to the top
noteDetailWrapperEl.scrollTop(0);
loadAttributeList();
}
async function loadAttributeList() {
const noteId = getCurrentNoteId();
const attributes = await server.get('notes/' + noteId + '/attributes');
attributeListInnerEl.html('');
if (attributes.length > 0) {
for (const attr of attributes) {
attributeListInnerEl.append(formatAttribute(attr) + " ");
}
attributeListEl.show();
}
else {
attributeListEl.hide();
}
}
async function loadNote(noteId) {
@@ -290,6 +316,8 @@ const noteEditor = (function() {
newNoteCreated,
getEditor,
focus,
executeCurrentNote
executeCurrentNote,
loadAttributeList,
setContent
};
})();

View File

@@ -3,7 +3,7 @@
const noteTree = (function() {
const treeEl = $("#tree");
const parentListEl = $("#parent-list");
const parentListListEl = $("#parent-list-list");
const parentListListEl = $("#parent-list-inner");
let startNotePath = null;
let notesTreeMap = {};
@@ -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
}
})();

View File

@@ -116,5 +116,20 @@ async function stopWatch(what, func) {
}
function executeScript(script) {
eval("(async function() {" + script + "})()");
// last \r\n is necessary if script contains line comment on its last line
eval("(async function() {" + script + "\r\n})()");
}
function formatValueWithWhitespace(val) {
return /[^\w_-]/.test(val) ? '"' + val + '"' : val;
}
function formatAttribute(attr) {
let str = "@" + formatValueWithWhitespace(attr.name);
if (attr.value !== "") {
str += "=" + formatValueWithWhitespace(attr.value);
}
return str;
}

File diff suppressed because one or more lines are too long

View File

@@ -5,12 +5,18 @@
display: grid;
grid-template-areas: "header header"
"tree-actions title"
"search note-content"
"tree note-content"
"parent-list note-content";
"parent-list note-content"
"parent-list attribute-list";
grid-template-columns: 2fr 5fr;
grid-template-rows: auto
auto
1fr;
auto
1fr
auto
auto;
justify-content: center;
grid-gap: 10px;
}
@@ -108,7 +114,7 @@ span.fancytree-active:not(.fancytree-focused) .fancytree-title {
}
#header-title {
padding: 5px 50px 5px 10px;
padding: 5px 20px 5px 10px;
font-size: large;
font-weight: bold;
}
@@ -134,6 +140,7 @@ div.ui-tooltip {
margin-left: 20px;
border-top: 2px solid #eee;
padding-top: 10px;
grid-area: parent-list;
}
#parent-list ul {
@@ -190,11 +197,6 @@ div.ui-tooltip {
float: right;
}
#note-id-display {
color: lightgrey;
margin-left: 10px;
}
#note-source {
height: 98%;
width: 100%;
@@ -243,8 +245,9 @@ div.ui-tooltip {
#note-id-display {
position: absolute;
right: 10px;
bottom: 5px;
bottom: 8px;
z-index: 1000;
color: lightgrey;
}
#note-type-dropdown {
@@ -253,4 +256,16 @@ div.ui-tooltip {
overflow-x: hidden;
}
.cm-matchhighlight {background-color: #eeeeee}
.cm-matchhighlight {background-color: #eeeeee}
#attribute-list {
grid-area: attribute-list;
color: #777777;
border-top: 1px solid #eee;
padding: 5px; display: none;
}
#attribute-list button {
padding: 2px;
margin-right: 5px;
}

View File

@@ -7,14 +7,15 @@ const auth = require('../../services/auth');
const sync_table = require('../../services/sync_table');
const utils = require('../../services/utils');
const wrap = require('express-promise-wrap').wrap;
const attributes = require('../../services/attributes');
router.get('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
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('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
const noteId = req.params.noteId;
const attributes = req.body;
const now = utils.nowDate();
@@ -22,19 +23,26 @@ router.put('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next)
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
});
}
@@ -42,7 +50,29 @@ router.put('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next)
}
});
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 WHERE isDeleted = 0");
for (const attr of attributes.BUILTIN_ATTRIBUTES) {
if (!names.includes(attr)) {
names.push(attr);
}
}
names.sort();
res.send(names);
}));
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 isDeleted = 0 AND name = ? AND value != '' ORDER BY value", [attributeName]);
res.send(values);
}));
module.exports = router;

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

@@ -58,15 +58,114 @@ router.put('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
}));
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
const search = '%' + utils.sanitizeSql(req.query.search) + '%';
let {attrFilters, searchText} = parseFilters(req.query.search);
// searching in protected notes is pointless because of encryption
const noteIds = await sql.getColumn(`SELECT noteId FROM notes
WHERE isDeleted = 0 AND isProtected = 0 AND (title LIKE ? OR content LIKE ?)`, [search, search]);
const {query, params} = getSearchQuery(attrFilters, searchText);
console.log(query, params);
const noteIds = await sql.getColumn(query, params);
res.send(noteIds);
}));
function parseFilters(searchText) {
const attrFilters = [];
const attrRegex = /(\b(and|or)\s+)?@(!?)([\w_-]+|"[^"]+")((=|!=|<|<=|>|>=)([\w_-]+|"[^"]+"))?/i;
let match = attrRegex.exec(searchText);
function trimQuotes(str) { return str.startsWith('"') ? str.substr(1, str.length - 2) : str; }
while (match != null) {
const relation = match[2] !== undefined ? match[2].toLowerCase() : 'and';
const operator = match[3] === '!' ? 'not-exists' : 'exists';
attrFilters.push({
relation: relation,
name: trimQuotes(match[4]),
operator: match[6] !== undefined ? match[6] : operator,
value: match[7] !== undefined ? trimQuotes(match[7]) : null
});
// remove attributes from further fulltext search
searchText = searchText.split(match[0]).join('');
match = attrRegex.exec(searchText);
}
return {attrFilters, searchText};
}
function getSearchQuery(attrFilters, searchText) {
const joins = [];
const joinParams = [];
let where = '1';
const whereParams = [];
let i = 1;
for (const filter of attrFilters) {
joins.push(`LEFT JOIN attributes AS attr${i} ON attr${i}.noteId = notes.noteId AND attr${i}.name = ?`);
joinParams.push(filter.name);
where += " " + filter.relation + " ";
if (filter.operator === 'exists') {
where += `attr${i}.attributeId IS NOT NULL`;
}
else if (filter.operator === 'not-exists') {
where += `attr${i}.attributeId IS NULL`;
}
else if (filter.operator === '=' || filter.operator === '!=') {
where += `attr${i}.value ${filter.operator} ?`;
whereParams.push(filter.value);
}
else if ([">", ">=", "<", "<="].includes(filter.operator)) {
const floatParam = parseFloat(filter.value);
if (isNaN(floatParam)) {
where += `attr${i}.value ${filter.operator} ?`;
whereParams.push(filter.value);
}
else {
where += `CAST(attr${i}.value AS DECIMAL) ${filter.operator} ?`;
whereParams.push(floatParam);
}
}
else {
throw new Error("Unknown operator " + filter.operator);
}
i++;
}
let searchCondition = '';
const searchParams = [];
if (searchText.trim() !== '') {
// searching in protected notes is pointless because of encryption
searchCondition = ' AND (notes.isProtected = 0 AND (notes.title LIKE ? OR notes.content LIKE ?))';
searchText = '%' + searchText.trim() + '%';
searchParams.push(searchText);
searchParams.push(searchText); // two occurences in searchCondition
}
const query = `SELECT DISTINCT notes.noteId FROM notes
${joins.join('\r\n')}
WHERE
notes.isDeleted = 0
AND (${where})
${searchCondition}`;
const params = joinParams.concat(whereParams).concat(searchParams);
return { query, params };
}
router.put('/:noteId/sort', auth.checkApiAuth, wrap(async (req, res, next) => {
const noteId = req.params.noteId;
const sourceId = req.headers.source_id;

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;

View File

@@ -19,11 +19,12 @@ router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => {
router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => {
const noteIds = await attributes.getNoteIdsWithAttribute("run_on_startup");
const repository = new Repository(req);
const scripts = [];
for (const noteId of noteIds) {
scripts.push(await getNoteWithSubtreeScript(noteId, req));
scripts.push(await getNoteWithSubtreeScript(noteId, repository));
}
res.send(scripts);
@@ -41,10 +42,10 @@ router.get('/subtree/:noteId', auth.checkApiAuth, wrap(async (req, res, next) =>
res.send(subTreeScripts + noteScript);
}));
async function getNoteWithSubtreeScript(noteId, req) {
const noteScript = (await notes.getNoteById(noteId, req)).content;
async function getNoteWithSubtreeScript(noteId, repository) {
const noteScript = (await repository.getNote(noteId)).content;
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], req);
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository);
return subTreeScripts + noteScript;
}

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);
@@ -40,7 +41,7 @@ function register(app) {
app.use('/api/notes', notesApiRoute);
app.use('/api/tree', treeChangesApiRoute);
app.use('/api/notes', cloningApiRoute);
app.use('/api/notes', attributesRoute);
app.use('/api', attributesRoute);
app.use('/api/notes-history', noteHistoryApiRoute);
app.use('/api/recent-changes', recentChangesApiRoute);
app.use('/api/settings', settingsApiRoute);
@@ -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 = {

13
src/scripts/today.js Normal file
View File

@@ -0,0 +1,13 @@
api.addButtonToToolbar('go-today', $('<button class="btn btn-xs" onclick="goToday();"><span class="ui-icon ui-icon-calendar"></span> Today</button>'));
window.goToday = async function() {
const todayDateStr = formatDateISO(new Date());
const todayNoteId = await server.exec([todayDateStr], async todayDateStr => {
return await this.getDateNoteId(todayDateStr);
});
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
}
});
}
@@ -65,10 +66,14 @@
});
}
data.sort((a, b) => a.date < b.date ? -1 : +1);
return data;
});
var config = {
const ctx = $("#canvas")[0].getContext("2d");
new Chart(ctx, {
type: 'line',
data: {
labels: data.map(row => row.date),
@@ -80,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,13 +5,23 @@ const utils = require('./utils');
const sync_table = require('./sync_table');
const Repository = require('./repository');
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]);
}
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) {
@@ -21,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;
@@ -39,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) {
@@ -52,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);
@@ -64,5 +75,6 @@ module.exports = {
getNotesWithAttribute,
getNoteWithAttribute,
getNoteIdsWithAttribute,
createAttribute
createAttribute,
BUILTIN_ATTRIBUTES
};

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

@@ -5,32 +5,6 @@ const sync_table = require('./sync_table');
const attributes = require('./attributes');
const protected_session = require('./protected_session');
async function updateJsonNote(noteId, data) {
const ret = await createNewNote(noteId, {
title: name,
content: JSON.stringify(data),
target: 'into',
isProtected: false,
type: 'code',
mime: 'application/json'
});
return ret.noteId;
}
async function createNewJsonNote(parentNoteId, name, payload) {
const ret = await createNewNote(parentNoteId, {
title: name,
content: JSON.stringify(payload),
target: 'into',
isProtected: false,
type: 'code',
mime: 'application/json'
});
return ret.noteId;
}
async function createNewNote(parentNoteId, noteOpts, dataKey, sourceId) {
const noteId = utils.newNoteId();
const noteTreeId = utils.newNoteTreeId();
@@ -180,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,
@@ -193,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) {
@@ -261,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

@@ -17,15 +17,20 @@
<button class="btn btn-xs" onclick="jumpToNote.showDialog();" title="CTRL+J">Jump to note</button>
<button class="btn btn-xs" onclick="recentNotes.showDialog();" title="CTRL+E">Recent notes</button>
<button class="btn btn-xs" onclick="recentChanges.showDialog();">Recent changes</button>
<button class="btn btn-xs" onclick="eventLog.showDialog();">Event log</button>
</div>
<div id="plugin-buttons">
</div>
<div>
<button class="btn btn-xs" onclick="syncNow();" title="Number of outstanding changes to be pushed to server">
<span class="ui-icon ui-icon-refresh"></span>
Sync now (<span id="changes-to-push-count">0</span>)
</button>
<button class="btn btn-xs" onclick="settings.showDialog();">Settings</button>
<button class="btn btn-xs" onclick="settings.showDialog();">
<span class="ui-icon ui-icon-gear"></span> Settings</button>
<form action="logout" id="logout-button" method="POST" style="display: inline;">
<input type="submit" class="btn btn-xs" value="Logout">
@@ -51,14 +56,13 @@
<img src="images/icons/search.png" alt="Search in notes"/>
</a>
</div>
</div>
<div id="search-box" style="display: none; padding: 10px; margin-top: 10px;">
<p>
<label>Search:</label>
<input name="search-text" autocomplete="off">
<button id="reset-search-button">&times;</button>
<span id="matches"></span>
</p>
<div id="search-box" class="hide-toggle" style="grid-area: search; display: none; padding: 10px; margin-top: 10px;">
<div style="display: flex; align-items: center;">
<label>Search:</label>
<input name="search-text" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off">
<button id="reset-search-button" class="btn btn-sm" title="Reset search">&times;</button>
</div>
</div>
@@ -68,7 +72,7 @@
<div id="parent-list" class="hide-toggle">
<p><strong>Note locations:</strong></p>
<ul id="parent-list-list"></ul>
<ul id="parent-list-inner"></ul>
</div>
<div class="hide-toggle" style="grid-area: title;">
@@ -138,23 +142,16 @@
<div id="note-detail-render"></div>
</div>
<div id="attribute-list">
<button class="btn btn-sm" onclick="attributesDialog.showDialog();">Attributes:</button>
<span id="attribute-list-inner"></span>
</div>
</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;">
@@ -363,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>
@@ -378,29 +378,40 @@
<div id="attributes-dialog" title="Note attributes" style="display: none; padding: 20px;">
<form data-bind="submit: save">
<div style="display: flex; justify-content: space-between; padding: 15px; padding-top: 0;">
<button class="btn-default" type="button" data-bind="click: addNewRow">Add new attribute</button>
<button class="btn-primary" type="submit">Save</button>
<div style="text-align: center">
<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>
<input type="text" data-bind="value: name"/>
<!-- 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: 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" data-bind="value: value" style="width: 300px"/>
<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>
@@ -492,7 +503,7 @@
<script src="javascripts/link.js"></script>
<script src="javascripts/sync.js"></script>
<script src="javascripts/messaging.js"></script>
<script src="javascripts/api.js"></script>
<script type="text/javascript">
// we hide container initally because otherwise it is rendered first without CSS and then flickers into