Compare commits

..

18 Commits

Author SHA1 Message Date
azivner
e8c52e25f0 release 0.7.0-beta 2018-02-19 23:03:30 -05:00
azivner
a149c6a105 lazy / dynamic loading of CKEditor and Code mirror 2018-02-19 22:02:03 -05:00
azivner
131af9ab12 fix attachment sync 2018-02-18 22:55:36 -05:00
azivner
aa2bbc6575 attachment download now works also in electron, added option to open the attachment 2018-02-18 22:19:07 -05:00
azivner
78e8c15786 attachment upload and download now works for browser 2018-02-18 21:28:24 -05:00
azivner
fda4146150 correct handling of inclusion of dependencies 2018-02-18 10:47:02 -05:00
azivner
ddc885066e support passing functions to the backend as parameters 2018-02-18 09:53:36 -05:00
azivner
08bc2afb49 now it's possible to add comment to the weight, closes #54 2018-02-17 11:47:22 -05:00
azivner
1d0220b03d add weight causes updating old chart instead of creating new chart, closes #53 2018-02-17 10:45:00 -05:00
azivner
3033f7cc08 attribute value is now non-null, fixes #52 2018-02-16 19:07:59 -05:00
azivner
6b9ff47c88 Merge branch 'stable' 2018-02-15 23:24:02 -05:00
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
cdde6a4d8e file/attachment upload, wiP 2018-02-14 23:31:20 -05:00
34 changed files with 722 additions and 301 deletions

View File

@@ -0,0 +1,23 @@
UPDATE attributes SET value = '' WHERE value IS NULL;
CREATE TABLE IF NOT EXISTS "attributes_mig"
(
attributeId TEXT PRIMARY KEY NOT NULL,
noteId TEXT NOT NULL,
name TEXT NOT NULL,
value TEXT NOT NULL DEFAULT '',
position INT NOT NULL DEFAULT 0,
dateCreated TEXT NOT NULL,
dateModified TEXT NOT NULL,
isDeleted INT NOT NULL
);
INSERT INTO attributes_mig (attributeId, noteId, name, value, position, dateCreated, dateModified, isDeleted)
SELECT attributeId, noteId, name, value, position, dateCreated, dateModified, isDeleted FROM attributes;
DROP TABLE attributes;
ALTER TABLE attributes_mig RENAME TO attributes;
CREATE INDEX IDX_attributes_noteId ON attributes (noteId);
CREATE INDEX IDX_attributes_name_value ON attributes (name, value);

View File

@@ -3,11 +3,11 @@
const electron = require('electron'); const electron = require('electron');
const path = require('path'); const path = require('path');
const config = require('./src/services/config'); const config = require('./src/services/config');
const log = require('./src/services/log');
const url = require("url"); const url = require("url");
const app = electron.app; const app = electron.app;
const globalShortcut = electron.globalShortcut; const globalShortcut = electron.globalShortcut;
const clipboard = electron.clipboard;
// Adds debug features like hotkeys for triggering dev tools and reload // Adds debug features like hotkeys for triggering dev tools and reload
require('electron-debug')(); require('electron-debug')();
@@ -15,6 +15,8 @@ require('electron-debug')();
// Prevent window being garbage collected // Prevent window being garbage collected
let mainWindow; let mainWindow;
require('electron-dl')({ saveAs: true });
function onClosed() { function onClosed() {
// Dereference the window // Dereference the window
// For multiple windows store them in an array // For multiple windows store them in an array
@@ -70,7 +72,7 @@ app.on('activate', () => {
app.on('ready', () => { app.on('ready', () => {
mainWindow = createMainWindow(); mainWindow = createMainWindow();
globalShortcut.register('CommandOrControl+Alt+P', async () => { const result = globalShortcut.register('CommandOrControl+Alt+P', async () => {
const date_notes = require('./src/services/date_notes'); const date_notes = require('./src/services/date_notes');
const utils = require('./src/services/utils'); const utils = require('./src/services/utils');
@@ -81,6 +83,10 @@ app.on('ready', () => {
mainWindow.webContents.send('create-day-sub-note', parentNoteId); mainWindow.webContents.send('create-day-sub-note', parentNoteId);
}); });
if (!result) {
log.error("Could not register global shortcut CTRL+ALT+P");
}
}); });
app.on('will-quit', () => { app.on('will-quit', () => {

148
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "trilium", "name": "trilium",
"version": "0.4.1", "version": "0.6.2",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -3061,19 +3061,19 @@
"integrity": "sha1-zIcsFoiArjxxiXYv1f/ACJbJUYo=" "integrity": "sha1-zIcsFoiArjxxiXYv1f/ACJbJUYo="
}, },
"electron": { "electron": {
"version": "1.8.2-beta.4", "version": "1.8.2",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.8.2-beta.4.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-1.8.2.tgz",
"integrity": "sha1-GDayBO6s6dx3Bi7Ugg/bxsvZoZU=", "integrity": "sha512-0TV5Hy92g8ACnPn+PVol6a/2uk+khzmRtWxhah/FcKs6StCytm5hD14QqOdZxEdJN8HljXIVCayN/wJX+0wDiQ==",
"requires": { "requires": {
"@types/node": "8.5.9", "@types/node": "8.9.4",
"electron-download": "3.3.0", "electron-download": "3.3.0",
"extract-zip": "1.6.5" "extract-zip": "1.6.5"
}, },
"dependencies": { "dependencies": {
"@types/node": { "@types/node": {
"version": "8.5.9", "version": "8.9.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.5.9.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.4.tgz",
"integrity": "sha512-s+c3AjymyAccTI4hcgNFK4mToH8l+hyPDhu4LIkn71lRy56FLijGu00fyLgldjM/846Pmk9N4KFUs2P8GDs0pA==" "integrity": "sha512-dSvD36qnQs78G1BPsrZFdPpvLgMW/dnvr5+nTW2csMs5TiP9MOXrjUbnMZOEwnIuBklXtn7b6TPA2Cuq07bDHA=="
} }
} }
}, },
@@ -3206,6 +3206,16 @@
"electron-localshortcut": "3.1.0" "electron-localshortcut": "3.1.0"
} }
}, },
"electron-dl": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-1.11.0.tgz",
"integrity": "sha512-iL9qHzzWOuL9bus+UT+P72SwrDQcFTV6QHqcbhwgqjCC9/K5jhdRzG0dIMB3TzYlk6rmApanPqh9DvWykwIH1Q==",
"requires": {
"ext-name": "5.0.0",
"pupa": "1.0.0",
"unused-filename": "1.0.0"
}
},
"electron-download": { "electron-download": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz", "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz",
@@ -3325,9 +3335,9 @@
} }
}, },
"electron-packager": { "electron-packager": {
"version": "10.1.1", "version": "11.0.1",
"resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-10.1.1.tgz", "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-11.0.1.tgz",
"integrity": "sha1-MWp/ossf/CYz9YBcn8IJE8vAnZQ=", "integrity": "sha1-wtH/nsqBEL6evIGCbiqSHATRIA4=",
"dev": true, "dev": true,
"requires": { "requires": {
"asar": "0.14.0", "asar": "0.14.0",
@@ -3343,13 +3353,19 @@
"pify": "3.0.0", "pify": "3.0.0",
"plist": "2.1.0", "plist": "2.1.0",
"pruner": "0.0.7", "pruner": "0.0.7",
"rcedit": "0.9.0", "rcedit": "1.0.0",
"resolve": "1.4.0", "resolve": "1.4.0",
"sanitize-filename": "1.6.1", "sanitize-filename": "1.6.1",
"semver": "5.4.1", "semver": "5.4.1",
"yargs-parser": "8.1.0" "yargs-parser": "9.0.2"
}, },
"dependencies": { "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": { "electron-download": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/electron-download/-/electron-download-4.1.0.tgz", "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-4.1.0.tgz",
@@ -3437,6 +3453,12 @@
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true "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": { "sumchecker": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-2.0.2.tgz", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-2.0.2.tgz",
@@ -3456,20 +3478,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": { "electron-prebuilt-compile": {
"version": "1.8.2-beta.4", "version": "1.8.2",
"resolved": "https://registry.npmjs.org/electron-prebuilt-compile/-/electron-prebuilt-compile-1.8.2-beta.4.tgz", "resolved": "https://registry.npmjs.org/electron-prebuilt-compile/-/electron-prebuilt-compile-1.8.2.tgz",
"integrity": "sha512-whVdRgFEDovWSFrAsbMXIiush6RQ8IV3XhYdL59zShck4U1eXGmdkaBCy+2tlkGmUGr0fRu+S4FpUx2ebBkRhQ==", "integrity": "sha512-wiDVjy8S0PA/K/TUM0lw5gzZ+SmyVVGQ0qt9iFYXHJc6t8TzDXFY3DsoK37H3A7nWnkvXvoPdpJ5/h9KbTMoAw==",
"dev": true, "dev": true,
"requires": { "requires": {
"babel-plugin-array-includes": "2.0.3", "babel-plugin-array-includes": "2.0.3",
"babel-plugin-transform-async-to-generator": "6.24.1", "babel-plugin-transform-async-to-generator": "6.24.1",
"babel-preset-es2016-node5": "1.1.2", "babel-preset-es2016-node5": "1.1.2",
"babel-preset-react": "6.24.1", "babel-preset-react": "6.24.1",
"electron": "1.8.2-beta.4", "electron": "1.8.2",
"electron-compile": "6.4.2", "electron-compile": "6.4.2",
"electron-compilers": "5.9.0", "electron-compilers": "5.9.0",
"yargs": "6.6.0" "yargs": "6.6.0"
@@ -4353,6 +4384,23 @@
} }
} }
}, },
"ext-list": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz",
"integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==",
"requires": {
"mime-db": "1.30.0"
}
},
"ext-name": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz",
"integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==",
"requires": {
"ext-list": "2.2.2",
"sort-keys-length": "1.0.1"
}
},
"extend": { "extend": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
@@ -5901,8 +5949,7 @@
"is-plain-obj": { "is-plain-obj": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4="
"dev": true
}, },
"is-png": { "is-png": {
"version": "1.1.0", "version": "1.1.0",
@@ -7131,6 +7178,11 @@
} }
} }
}, },
"modify-filename": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz",
"integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE="
},
"moment": { "moment": {
"version": "2.20.1", "version": "2.20.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz",
@@ -7543,6 +7595,11 @@
"mimic-fn": "1.1.0" "mimic-fn": "1.1.0"
} }
}, },
"open": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz",
"integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw="
},
"optimist": { "optimist": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
@@ -8370,6 +8427,11 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
}, },
"pupa": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/pupa/-/pupa-1.0.0.tgz",
"integrity": "sha1-mpVopa9+ZXuEYqbp1TKHQ1YM7/Y="
},
"q": { "q": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@@ -8472,12 +8534,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": { "read-all-stream": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz",
@@ -9171,11 +9227,18 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
"integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=",
"dev": true,
"requires": { "requires": {
"is-plain-obj": "1.1.0" "is-plain-obj": "1.1.0"
} }
}, },
"sort-keys-length": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz",
"integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=",
"requires": {
"sort-keys": "1.1.2"
}
},
"source-map": { "source-map": {
"version": "0.5.7", "version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -10948,6 +11011,22 @@
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
}, },
"unused-filename": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-1.0.0.tgz",
"integrity": "sha1-00CID3GuIRXrqhMlvvBcxmhEacY=",
"requires": {
"modify-filename": "1.1.0",
"path-exists": "3.0.0"
},
"dependencies": {
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
}
}
},
"unzip-response": { "unzip-response": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz",
@@ -11694,23 +11773,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": { "yauzl": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",

View File

@@ -1,7 +1,7 @@
{ {
"name": "trilium", "name": "trilium",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.6.1", "version": "0.7.0-beta",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"repository": { "repository": {
@@ -27,8 +27,9 @@
"debug": "~3.1.0", "debug": "~3.1.0",
"devtron": "^1.4.0", "devtron": "^1.4.0",
"ejs": "~2.5.7", "ejs": "~2.5.7",
"electron": "^1.8.2-beta.4", "electron": "^1.8.2",
"electron-debug": "^1.5.0", "electron-debug": "^1.5.0",
"electron-dl": "^1.11.0",
"electron-in-page-search": "^1.2.4", "electron-in-page-search": "^1.2.4",
"express": "~4.16.2", "express": "~4.16.2",
"express-promise-wrap": "^0.2.2", "express-promise-wrap": "^0.2.2",
@@ -45,6 +46,7 @@
"jimp": "^0.2.28", "jimp": "^0.2.28",
"moment": "^2.20.1", "moment": "^2.20.1",
"multer": "^1.3.0", "multer": "^1.3.0",
"open": "0.0.5",
"rand-token": "^0.4.0", "rand-token": "^0.4.0",
"request": "^2.83.0", "request": "^2.83.0",
"request-promise": "^4.2.2", "request-promise": "^4.2.2",
@@ -60,8 +62,8 @@
}, },
"devDependencies": { "devDependencies": {
"electron-compile": "^6.4.2", "electron-compile": "^6.4.2",
"electron-packager": "^10.1.1", "electron-packager": "^11.0.1",
"electron-prebuilt-compile": "1.8.2-beta.4", "electron-prebuilt-compile": "1.8.2",
"electron-rebuild": "^1.7.3", "electron-rebuild": "^1.7.3",
"tape": "^4.8.0", "tape": "^4.8.0",
"xo": "^0.18.0" "xo": "^0.18.0"

View File

@@ -23,6 +23,10 @@ class Note extends Entity {
return this.type === "code" && this.mime === "application/json"; return this.type === "code" && this.mime === "application/json";
} }
isJavaScript() {
return this.type === "code" && this.mime === "application/javascript";
}
async getAttributes() { async getAttributes() {
return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]); return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
} }

View File

@@ -1,5 +1,5 @@
const api = (function() { const api = (function() {
const pluginButtonsEl = $("#plugin-buttons"); const $pluginButtons = $("#plugin-buttons");
async function activateNote(notePath) { async function activateNote(notePath) {
await noteTree.activateNode(notePath); await noteTree.activateNode(notePath);
@@ -10,7 +10,7 @@ const api = (function() {
button.attr('id', buttonId); button.attr('id', buttonId);
pluginButtonsEl.append(button); $pluginButtons.append(button);
} }

View File

@@ -1,7 +1,7 @@
"use strict"; "use strict";
const contextMenu = (function() { const contextMenu = (function() {
const treeEl = $("#tree"); const $tree = $("#tree");
let clipboardIds = []; let clipboardIds = [];
let clipboardMode = null; let clipboardMode = null;
@@ -93,8 +93,8 @@ const contextMenu = (function() {
beforeOpen: (event, ui) => { beforeOpen: (event, ui) => {
const node = $.ui.fancytree.getNode(ui.target); const node = $.ui.fancytree.getNode(ui.target);
// Modify menu entries depending on node status // Modify menu entries depending on node status
treeEl.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0); $tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0);
treeEl.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0); $tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0);
// Activate node on right-click // Activate node on right-click
node.setActive(); node.setActive();

View File

@@ -17,26 +17,34 @@ const sqlConsole = (function() {
width: $(window).width(), width: $(window).width(),
height: $(window).height(), height: $(window).height(),
open: function() { open: function() {
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; initEditor();
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 initEditor() {
if (!codeEditor) {
await requireLibrary(CODE_MIRROR);
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() { async function execute() {
const sqlQuery = codeEditor.getValue(); const sqlQuery = codeEditor.getValue();

View File

@@ -207,8 +207,32 @@ if (isElectron()) {
await noteTree.activateNode(parentNoteId); await noteTree.activateNode(parentNoteId);
const node = noteTree.getCurrentNode(); setTimeout(() => {
const node = noteTree.getCurrentNode();
await noteTree.createNote(node, node.data.noteId, 'into', node.data.isProtected); noteTree.createNote(node, node.data.noteId, 'into', node.data.isProtected);
}, 500);
}); });
} }
function uploadAttachment() {
$("#file-upload").trigger('click');
}
$("#file-upload").change(async function() {
const formData = new FormData();
formData.append('upload', this.files[0]);
const resp = await $.ajax({
url: baseApiUrl + 'attachments/upload/' + noteEditor.getCurrentNoteId(),
headers: server.getHeaders(),
data: formData,
type: 'POST',
contentType: false, // NEEDED, DON'T OMIT THIS
processData: false, // NEEDED, DON'T OMIT THIS
});
await noteTree.reload();
await noteTree.activateNode(resp.noteId);
});

View File

@@ -41,11 +41,11 @@ const link = (function() {
function goToLink(e) { function goToLink(e) {
e.preventDefault(); e.preventDefault();
const linkEl = $(e.target); const $link = $(e.target);
let notePath = linkEl.attr("note-path"); let notePath = $link.attr("note-path");
if (!notePath) { if (!notePath) {
const address = linkEl.attr("note-path") ? linkEl.attr("note-path") : linkEl.attr('href'); const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href');
if (!address) { if (!address) {
return; return;

View File

@@ -1,7 +1,7 @@
"use strict"; "use strict";
const messaging = (function() { const messaging = (function() {
const changesToPushCountEl = $("#changes-to-push-count"); const $changesToPushCount = $("#changes-to-push-count");
function logError(message) { function logError(message) {
console.log(now(), message); // needs to be separate from .trace() console.log(now(), message); // needs to be separate from .trace()
@@ -52,7 +52,7 @@ const messaging = (function() {
// we don't detect image changes here since images themselves are immutable and references should be // we don't detect image changes here since images themselves are immutable and references should be
// updated in note detail as well // updated in note detail as well
changesToPushCountEl.html(message.changesToPushCount); $changesToPushCount.html(message.changesToPushCount);
} }
else if (message.type === 'sync-hash-check-failed') { else if (message.type === 'sync-hash-check-failed') {
showError("Sync check failed!", 60000); showError("Sync check failed!", 60000);

View File

@@ -1,16 +1,24 @@
"use strict"; "use strict";
const noteEditor = (function() { const noteEditor = (function() {
const noteTitleEl = $("#note-title"); const $noteTitle = $("#note-title");
const noteDetailEl = $('#note-detail');
const noteDetailCodeEl = $('#note-detail-code'); const $noteDetail = $('#note-detail');
const noteDetailRenderEl = $('#note-detail-render'); const $noteDetailCode = $('#note-detail-code');
const protectButton = $("#protect-button"); const $noteDetailRender = $('#note-detail-render');
const unprotectButton = $("#unprotect-button"); const $noteDetailAttachment = $('#note-detail-attachment');
const noteDetailWrapperEl = $("#note-detail-wrapper");
const noteIdDisplayEl = $("#note-id-display"); const $protectButton = $("#protect-button");
const attributeListEl = $("#attribute-list"); const $unprotectButton = $("#unprotect-button");
const attributeListInnerEl = $("#attribute-list-inner"); const $noteDetailWrapper = $("#note-detail-wrapper");
const $noteIdDisplay = $("#note-id-display");
const $attributeList = $("#attribute-list");
const $attributeListInner = $("#attribute-list-inner");
const $attachmentFileName = $("#attachment-filename");
const $attachmentFileType = $("#attachment-filetype");
const $attachmentFileSize = $("#attachment-filesize");
const $attachmentDownload = $("#attachment-download");
const $attachmentOpen = $("#attachment-open");
let editor = null; let editor = null;
let codeEditor = null; let codeEditor = null;
@@ -80,14 +88,14 @@ const noteEditor = (function() {
else if (note.detail.type === 'code') { else if (note.detail.type === 'code') {
note.detail.content = codeEditor.getValue(); note.detail.content = codeEditor.getValue();
} }
else if (note.detail.type === 'render') { else if (note.detail.type === 'render' || note.detail.type === 'file') {
// nothing // nothing
} }
else { else {
throwError("Unrecognized type: " + note.detail.type); throwError("Unrecognized type: " + note.detail.type);
} }
const title = noteTitleEl.val(); const title = $noteTitle.val();
note.detail.title = title; note.detail.title = title;
@@ -105,9 +113,9 @@ const noteEditor = (function() {
function setNoteBackgroundIfProtected(note) { function setNoteBackgroundIfProtected(note) {
const isProtected = !!note.detail.isProtected; const isProtected = !!note.detail.isProtected;
noteDetailWrapperEl.toggleClass("protected", isProtected); $noteDetailWrapper.toggleClass("protected", isProtected);
protectButton.toggle(!isProtected); $protectButton.toggle(!isProtected);
unprotectButton.toggle(isProtected); $unprotectButton.toggle(isProtected);
} }
let isNewNoteCreated = false; let isNewNoteCreated = false;
@@ -116,19 +124,43 @@ const noteEditor = (function() {
isNewNoteCreated = true; isNewNoteCreated = true;
} }
function setContent(content) { async function setContent(content) {
if (currentNote.detail.type === 'text') { if (currentNote.detail.type === 'text') {
if (!editor) {
await requireLibrary(CKEDITOR);
editor = await BalloonEditor.create($noteDetail[0], {});
editor.document.on('change', noteChanged);
}
// temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49 // temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
editor.setData(content ? content : "<p></p>"); editor.setData(content ? content : "<p></p>");
noteDetailEl.show(); $noteDetail.show();
noteDetailCodeEl.hide();
noteDetailRenderEl.html('').hide();
} }
else if (currentNote.detail.type === 'code') { else if (currentNote.detail.type === 'code') {
noteDetailEl.hide(); if (!codeEditor) {
noteDetailCodeEl.show(); await requireLibrary(CODE_MIRROR);
noteDetailRenderEl.html('').hide();
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($("#note-detail-code")[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
matchBrackets: true,
matchTags: { bothTags: true },
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false }
});
codeEditor.on('change', noteChanged);
}
$noteDetailCode.show();
// this needs to happen after the element is shown, otherwise the editor won't be refresheds // this needs to happen after the element is shown, otherwise the editor won't be refresheds
codeEditor.setValue(content); codeEditor.setValue(content);
@@ -148,10 +180,10 @@ const noteEditor = (function() {
if (isNewNoteCreated) { if (isNewNoteCreated) {
isNewNoteCreated = false; isNewNoteCreated = false;
noteTitleEl.focus().select(); $noteTitle.focus().select();
} }
noteIdDisplayEl.html(noteId); $noteIdDisplay.html(noteId);
await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false); await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false);
@@ -163,26 +195,36 @@ const noteEditor = (function() {
// to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it. // to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it.
protected_session.ensureDialogIsClosed(); protected_session.ensureDialogIsClosed();
noteDetailWrapperEl.show(); $noteDetailWrapper.show();
noteChangeDisabled = true; noteChangeDisabled = true;
noteTitleEl.val(currentNote.detail.title); $noteTitle.val(currentNote.detail.title);
noteType.setNoteType(currentNote.detail.type); noteType.setNoteType(currentNote.detail.type);
noteType.setNoteMime(currentNote.detail.mime); noteType.setNoteMime(currentNote.detail.mime);
$noteDetail.hide();
$noteDetailCode.hide();
$noteDetailRender.html('').hide();
$noteDetailAttachment.hide();
if (currentNote.detail.type === 'render') { if (currentNote.detail.type === 'render') {
noteDetailEl.hide(); $noteDetailRender.show();
noteDetailCodeEl.hide();
noteDetailRenderEl.html('').show();
const subTree = await server.get('script/subtree/' + getCurrentNoteId()); const subTree = await server.get('script/subtree/' + getCurrentNoteId());
noteDetailRenderEl.html(subTree); $noteDetailRender.html(subTree);
}
else if (currentNote.detail.type === 'file') {
$noteDetailAttachment.show();
$attachmentFileName.text(currentNote.attributes.original_file_name);
$attachmentFileSize.text(currentNote.attributes.file_size + " bytes");
$attachmentFileType.text(currentNote.detail.mime);
} }
else { else {
setContent(currentNote.detail.content); await setContent(currentNote.detail.content);
} }
noteChangeDisabled = false; noteChangeDisabled = false;
@@ -191,7 +233,7 @@ const noteEditor = (function() {
noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId); noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
// after loading new note make sure editor is scrolled to the top // after loading new note make sure editor is scrolled to the top
noteDetailWrapperEl.scrollTop(0); $noteDetailWrapper.scrollTop(0);
loadAttributeList(); loadAttributeList();
} }
@@ -201,17 +243,17 @@ const noteEditor = (function() {
const attributes = await server.get('notes/' + noteId + '/attributes'); const attributes = await server.get('notes/' + noteId + '/attributes');
attributeListInnerEl.html(''); $attributeListInner.html('');
if (attributes.length > 0) { if (attributes.length > 0) {
for (const attr of attributes) { for (const attr of attributes) {
attributeListInnerEl.append(formatAttribute(attr) + " "); $attributeListInner.append(formatAttribute(attr) + " ");
} }
attributeListEl.show(); $attributeList.show();
} }
else { else {
attributeListEl.hide(); $attributeList.hide();
} }
} }
@@ -227,12 +269,12 @@ const noteEditor = (function() {
const note = getCurrentNote(); const note = getCurrentNote();
if (note.detail.type === 'text') { if (note.detail.type === 'text') {
noteDetailEl.focus(); $noteDetail.focus();
} }
else if (note.detail.type === 'code') { else if (note.detail.type === 'code') {
codeEditor.focus(); codeEditor.focus();
} }
else if (note.detail.type === 'render') { else if (note.detail.type === 'render' || note.detail.type === 'file') {
// do nothing // do nothing
} }
else { else {
@@ -257,45 +299,49 @@ const noteEditor = (function() {
} }
} }
$attachmentDownload.click(() => {
if (isElectron()) {
const remote = require('electron').remote;
remote.getCurrentWebContents().downloadURL(getAttachmentUrl());
}
else {
window.location.href = getAttachmentUrl();
}
});
$attachmentOpen.click(() => {
if (isElectron()) {
const open = require("open");
open(getAttachmentUrl());
}
else {
window.location.href = getAttachmentUrl();
}
});
function getAttachmentUrl() {
// electron needs absolute URL so we extract current host, port, protocol
const url = new URL(window.location.href);
const host = url.protocol + "//" + url.hostname + ":" + url.port;
const downloadUrl = "/api/attachments/download/" + getCurrentNoteId();
return host + downloadUrl;
}
$(document).ready(() => { $(document).ready(() => {
noteTitleEl.on('input', () => { $noteTitle.on('input', () => {
noteChanged(); noteChanged();
const title = noteTitleEl.val(); const title = $noteTitle.val();
noteTree.setNoteTitle(getCurrentNoteId(), title); noteTree.setNoteTitle(getCurrentNoteId(), title);
}); });
BalloonEditor
.create(document.querySelector('#note-detail'), {
})
.then(edit => {
editor = edit;
editor.document.on('change', noteChanged);
})
.catch(error => {
console.error(error);
});
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($("#note-detail-code")[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
matchBrackets: true,
matchTags: { bothTags: true },
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false }
});
codeEditor.on('change', noteChanged);
// so that tab jumps from note title (which has tabindex 1) // so that tab jumps from note title (which has tabindex 1)
noteDetailEl.attr("tabindex", 2); $noteDetail.attr("tabindex", 2);
}); });
$(document).bind('keydown', "ctrl+return", executeCurrentNote); $(document).bind('keydown', "ctrl+return", executeCurrentNote);

View File

@@ -1,9 +1,9 @@
"use strict"; "use strict";
const noteTree = (function() { const noteTree = (function() {
const treeEl = $("#tree"); const $tree = $("#tree");
const parentListEl = $("#parent-list"); const $parentList = $("#parent-list");
const parentListListEl = $("#parent-list-inner"); const $parentListList = $("#parent-list-inner");
let startNotePath = null; let startNotePath = null;
let notesTreeMap = {}; let notesTreeMap = {};
@@ -52,7 +52,7 @@ const noteTree = (function() {
// note that if you want to access data like noteId or isProtected, you need to go into "data" property // note that if you want to access data like noteId or isProtected, you need to go into "data" property
function getCurrentNode() { function getCurrentNode() {
return treeEl.fancytree("getActiveNode"); return $tree.fancytree("getActiveNode");
} }
function getCurrentNotePath() { function getCurrentNotePath() {
@@ -314,11 +314,11 @@ const noteTree = (function() {
} }
if (parents.length <= 1) { if (parents.length <= 1) {
parentListEl.hide(); $parentList.hide();
} }
else { else {
parentListEl.show(); $parentList.show();
parentListListEl.empty(); $parentListList.empty();
for (const parentNoteId of parents) { for (const parentNoteId of parents) {
const parentNotePath = getSomeNotePath(parentNoteId); const parentNotePath = getSomeNotePath(parentNoteId);
@@ -335,7 +335,7 @@ const noteTree = (function() {
item = link.createNoteLink(notePath, title); item = link.createNoteLink(notePath, title);
} }
parentListListEl.append($("<li/>").append(item)); $parentListList.append($("<li/>").append(item));
} }
} }
} }
@@ -543,7 +543,7 @@ const noteTree = (function() {
} }
}; };
treeEl.fancytree({ $tree.fancytree({
autoScroll: true, autoScroll: true,
keyboard: false, // we takover keyboard handling in the hotkeys plugin keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: ["hotkeys", "filter", "dnd", "clones"], extensions: ["hotkeys", "filter", "dnd", "clones"],
@@ -624,11 +624,11 @@ const noteTree = (function() {
} }
}); });
treeEl.contextmenu(contextMenu.contextMenuSettings); $tree.contextmenu(contextMenu.contextMenuSettings);
} }
function getTree() { function getTree() {
return treeEl.fancytree('getTree'); return $tree.fancytree('getTree');
} }
async function reload() { async function reload() {
@@ -663,7 +663,7 @@ const noteTree = (function() {
function collapseTree(node = null) { function collapseTree(node = null) {
if (!node) { if (!node) {
node = treeEl.fancytree("getRootNode"); node = $tree.fancytree("getRootNode");
} }
node.setExpanded(false); node.setExpanded(false);
@@ -744,7 +744,7 @@ const noteTree = (function() {
} }
async function createNewTopLevelNote() { async function createNewTopLevelNote() {
const rootNode = treeEl.fancytree("getRootNode"); const rootNode = $tree.fancytree("getRootNode");
await createNote(rootNode, "root", "into"); await createNote(rootNode, "root", "into");
} }

View File

@@ -1,7 +1,7 @@
"use strict"; "use strict";
const noteType = (function() { const noteType = (function() {
const executeScriptButton = $("#execute-script-button"); const $executeScriptButton = $("#execute-script-button");
const noteTypeModel = new NoteTypeModel(); const noteTypeModel = new NoteTypeModel();
function NoteTypeModel() { function NoteTypeModel() {
@@ -65,11 +65,18 @@ const noteType = (function() {
else if (type === 'render') { else if (type === 'render') {
return 'Render HTML note'; return 'Render HTML note';
} }
else if (type === 'file') {
return 'Attachment';
}
else { else {
throwError('Unrecognized type: ' + type); throwError('Unrecognized type: ' + type);
} }
}; };
this.isDisabled = function() {
return self.type() === "file";
};
async function save() { async function save() {
const note = noteEditor.getCurrentNote(); const note = noteEditor.getCurrentNote();
@@ -114,7 +121,7 @@ const noteType = (function() {
}; };
this.updateExecuteScriptButtonVisibility = function() { this.updateExecuteScriptButtonVisibility = function() {
executeScriptButton.toggle(self.mime() === 'application/javascript'); $executeScriptButton.toggle(self.mime() === 'application/javascript');
} }
} }

View File

@@ -1,10 +1,10 @@
"use strict"; "use strict";
const protected_session = (function() { const protected_session = (function() {
const dialogEl = $("#protected-session-password-dialog"); const $dialog = $("#protected-session-password-dialog");
const passwordFormEl = $("#protected-session-password-form"); const $passwordForm = $("#protected-session-password-form");
const passwordEl = $("#protected-session-password"); const $password = $("#protected-session-password");
const noteDetailWrapperEl = $("#note-detail-wrapper"); const $noteDetailWrapper = $("#note-detail-wrapper");
let protectedSessionDeferred = null; let protectedSessionDeferred = null;
let lastProtectedSessionOperationDate = null; let lastProtectedSessionOperationDate = null;
@@ -25,9 +25,9 @@ const protected_session = (function() {
if (requireProtectedSession && !isProtectedSessionAvailable()) { if (requireProtectedSession && !isProtectedSessionAvailable()) {
protectedSessionDeferred = dfd; protectedSessionDeferred = dfd;
noteDetailWrapperEl.hide(); $noteDetailWrapper.hide();
dialogEl.dialog({ $dialog.dialog({
modal: modal, modal: modal,
width: 400, width: 400,
open: () => { open: () => {
@@ -46,8 +46,8 @@ const protected_session = (function() {
} }
async function setupProtectedSession() { async function setupProtectedSession() {
const password = passwordEl.val(); const password = $password.val();
passwordEl.val(""); $password.val("");
const response = await enterProtectedSession(password); const response = await enterProtectedSession(password);
@@ -58,15 +58,15 @@ const protected_session = (function() {
protectedSessionId = response.protectedSessionId; protectedSessionId = response.protectedSessionId;
dialogEl.dialog("close"); $dialog.dialog("close");
noteEditor.reload(); noteEditor.reload();
noteTree.reload(); noteTree.reload();
if (protectedSessionDeferred !== null) { if (protectedSessionDeferred !== null) {
ensureDialogIsClosed(dialogEl, passwordEl); ensureDialogIsClosed($dialog, $password);
noteDetailWrapperEl.show(); $noteDetailWrapper.show();
protectedSessionDeferred.resolve(); protectedSessionDeferred.resolve();
@@ -77,11 +77,11 @@ const protected_session = (function() {
function ensureDialogIsClosed() { function ensureDialogIsClosed() {
// this may fal if the dialog has not been previously opened // this may fal if the dialog has not been previously opened
try { try {
dialogEl.dialog('close'); $dialog.dialog('close');
} }
catch (e) {} catch (e) {}
passwordEl.val(''); $password.val('');
} }
async function enterProtectedSession(password) { async function enterProtectedSession(password) {
@@ -155,7 +155,7 @@ const protected_session = (function() {
noteEditor.reload(); noteEditor.reload();
} }
passwordFormEl.submit(() => { $passwordForm.submit(() => {
setupProtectedSession(); setupProtectedSession();
return false; return false;

View File

@@ -1,40 +1,40 @@
"use strict"; "use strict";
const searchTree = (function() { const searchTree = (function() {
const treeEl = $("#tree"); const $tree = $("#tree");
const searchInputEl = $("input[name='search-text']"); const $searchInput = $("input[name='search-text']");
const resetSearchButton = $("button#reset-search-button"); const $resetSearchButton = $("button#reset-search-button");
const searchBoxEl = $("#search-box"); const $searchBox = $("#search-box");
resetSearchButton.click(resetSearch); $resetSearchButton.click(resetSearch);
function toggleSearch() { function toggleSearch() {
if (searchBoxEl.is(":hidden")) { if ($searchBox.is(":hidden")) {
searchBoxEl.show(); $searchBox.show();
searchInputEl.focus(); $searchInput.focus();
} }
else { else {
resetSearch(); resetSearch();
searchBoxEl.hide(); $searchBox.hide();
} }
} }
function resetSearch() { function resetSearch() {
searchInputEl.val(""); $searchInput.val("");
getTree().clearFilter(); getTree().clearFilter();
} }
function getTree() { function getTree() {
return treeEl.fancytree('getTree'); return $tree.fancytree('getTree');
} }
searchInputEl.keyup(async e => { $searchInput.keyup(async e => {
const searchText = searchInputEl.val(); const searchText = $searchInput.val();
if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") { if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") {
resetSearchButton.click(); $resetSearchButton.click();
return; return;
} }

View File

@@ -31,12 +31,23 @@ const server = (function() {
return await call('DELETE', url); return await call('DELETE', url);
} }
function prepareParams(params) {
return params.map(p => {
if (typeof p === "function") {
return "!@#Function: " + p.toString();
}
else {
return p;
}
});
}
async function exec(params, script) { async function exec(params, script) {
if (typeof script === "function") { if (typeof script === "function") {
script = script.toString(); script = script.toString();
} }
const ret = await post('script/exec', { script: script, params: params }); const ret = await post('script/exec', { script: script, params: prepareParams(params) });
return ret.executionResult; return ret.executionResult;
} }
@@ -105,6 +116,7 @@ const server = (function() {
put, put,
remove, remove,
exec, exec,
ajax,
// don't remove, used from CKEditor image upload! // don't remove, used from CKEditor image upload!
getHeaders getHeaders
} }

View File

@@ -1,14 +1,14 @@
"use strict"; "use strict";
const treeUtils = (function() { const treeUtils = (function() {
const treeEl = $("#tree"); const $tree = $("#tree");
function getParentProtectedStatus(node) { function getParentProtectedStatus(node) {
return isTopLevelNode(node) ? 0 : node.getParent().data.isProtected; return isTopLevelNode(node) ? 0 : node.getParent().data.isProtected;
} }
function getNodeByKey(key) { function getNodeByKey(key) {
return treeEl.fancytree('getNodeByKey', key); return $tree.fancytree('getNodeByKey', key);
} }
function getNoteIdFromNotePath(notePath) { function getNoteIdFromNotePath(notePath) {

View File

@@ -116,8 +116,7 @@ async function stopWatch(what, func) {
} }
function executeScript(script) { function executeScript(script) {
// last \r\n is necessary if script contains line comment on its last line eval(script);
eval("(async function() {" + script + "\r\n})()");
} }
function formatValueWithWhitespace(val) { function formatValueWithWhitespace(val) {
@@ -132,4 +131,57 @@ function formatAttribute(attr) {
} }
return str; return str;
}
const CKEDITOR = { "js": ["libraries/ckeditor/ckeditor.js"] };
const CODE_MIRROR = {
js: [
"libraries/codemirror/codemirror.js",
"libraries/codemirror/addon/mode/loadmode.js",
"libraries/codemirror/addon/fold/xml-fold.js",
"libraries/codemirror/addon/edit/matchbrackets.js",
"libraries/codemirror/addon/edit/matchtags.js",
"libraries/codemirror/addon/search/match-highlighter.js",
"libraries/codemirror/mode/meta.js"
],
css: [
"libraries/codemirror/codemirror.css"
]
};
async function requireLibrary(library) {
if (library.css) {
library.css.map(cssUrl => requireCss(cssUrl));
}
if (library.js) {
for (const scriptUrl of library.js) {
await requireScript(scriptUrl);
}
}
}
async function requireScript(url) {
const scripts = Array
.from(document.querySelectorAll('script'))
.map(scr => scr.src);
if (!scripts.includes(url)) {
return $.ajax({
url: url,
dataType: "script",
cache: true
})
}
}
async function requireCss(url) {
const css = Array
.from(document.querySelectorAll('link'))
.map(scr => scr.href);
if (!css.includes(url)) {
$('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url));
}
} }

View File

@@ -268,4 +268,9 @@ div.ui-tooltip {
#attribute-list button { #attribute-list button {
padding: 2px; padding: 2px;
margin-right: 5px; margin-right: 5px;
}
#attachment-table th, #attachment-table td {
padding: 10px;
font-size: large;
} }

View File

@@ -0,0 +1,61 @@
"use strict";
const express = require('express');
const router = express.Router();
const sql = require('../../services/sql');
const auth = require('../../services/auth');
const notes = require('../../services/notes');
const attributes = require('../../services/attributes');
const multer = require('multer')();
const wrap = require('express-promise-wrap').wrap;
router.post('/upload/:parentNoteId', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async (req, res, next) => {
const sourceId = req.headers.source_id;
const parentNoteId = req.params.parentNoteId;
const file = req.file;
const originalName = file.originalname;
const size = file.size;
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]);
if (!note) {
return res.status(404).send(`Note ${parentNoteId} doesn't exist.`);
}
await sql.doInTransaction(async () => {
const noteId = (await notes.createNewNote(parentNoteId, {
title: originalName,
content: file.buffer,
target: 'into',
isProtected: false,
type: 'file',
mime: file.mimetype
}, req, sourceId)).noteId;
await attributes.createAttribute(noteId, "original_file_name", originalName, sourceId);
await attributes.createAttribute(noteId, "file_size", size, sourceId);
res.send({
noteId: noteId
});
});
}));
router.get('/download/:noteId', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => {
const noteId = req.params.noteId;
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
if (!note) {
return res.status(404).send(`Note ${parentNoteId} doesn't exist.`);
}
const attributeMap = await attributes.getNoteAttributeMap(noteId);
const fileName = attributeMap.original_file_name ? attributeMap.original_file_name : note.title;
res.setHeader('Content-Disposition', 'attachment; filename=' + fileName);
res.setHeader('Content-Type', note.mime);
res.send(note.content);
}));
module.exports = router;

View File

@@ -5,6 +5,7 @@ const router = express.Router();
const auth = require('../../services/auth'); const auth = require('../../services/auth');
const sql = require('../../services/sql'); const sql = require('../../services/sql');
const notes = require('../../services/notes'); const notes = require('../../services/notes');
const attributes = require('../../services/attributes');
const log = require('../../services/log'); const log = require('../../services/log');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const protected_session = require('../../services/protected_session'); const protected_session = require('../../services/protected_session');
@@ -25,8 +26,19 @@ router.get('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
protected_session.decryptNote(req, detail); protected_session.decryptNote(req, detail);
let attributeMap = null;
if (detail.type === 'file') {
// no need to transfer attachment payload for this request
detail.content = null;
// attributes contain important attachment metadata - filename and size
attributeMap = await attributes.getNoteAttributeMap(noteId);
}
res.send({ res.send({
detail: detail detail: detail,
attributes: attributeMap
}); });
})); }));

View File

@@ -31,26 +31,28 @@ router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => {
})); }));
router.get('/subtree/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { router.get('/subtree/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
const repository = new Repository(req);
const noteId = req.params.noteId; const noteId = req.params.noteId;
const repository = new Repository(req); res.send(await getNoteWithSubtreeScript(noteId, repository));
const noteScript = (await repository.getNote(noteId)).content;
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository);
res.send(subTreeScripts + noteScript);
})); }));
async function getNoteWithSubtreeScript(noteId, repository) { async function getNoteWithSubtreeScript(noteId, repository) {
const noteScript = (await repository.getNote(noteId)).content; const note = await repository.getNote(noteId);
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository); let noteScript = note.content;
if (note.isJavaScript()) {
// last \r\n is necessary if script contains line comment on its last line
noteScript = "(async function() {" + noteScript + "\r\n})()";
}
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository, note.isJavaScript());
return subTreeScripts + noteScript; return subTreeScripts + noteScript;
} }
async function getSubTreeScripts(parentId, includedNoteIds, repository) { async function getSubTreeScripts(parentId, includedNoteIds, repository, isJavaScript) {
const children = await repository.getEntities(` const children = await repository.getEntities(`
SELECT notes.* SELECT notes.*
FROM notes JOIN note_tree USING(noteId) FROM notes JOIN note_tree USING(noteId)
@@ -69,7 +71,7 @@ async function getSubTreeScripts(parentId, includedNoteIds, repository) {
script += await getSubTreeScripts(child.noteId, includedNoteIds, repository); script += await getSubTreeScripts(child.noteId, includedNoteIds, repository);
if (child.mime === 'application/javascript') { if (!isJavaScript && child.mime === 'application/javascript') {
child.content = '<script>' + child.content + '</script>'; child.content = '<script>' + child.content + '</script>';
} }

View File

@@ -79,9 +79,12 @@ router.get('/changed', auth.checkApiAuth, wrap(async (req, res, next) => {
router.get('/notes/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { router.get('/notes/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const entity = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
sync.serializeNoteContentBuffer(entity);
res.send({ res.send({
entity: await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]) entity: entity
}); });
})); }));

View File

@@ -29,6 +29,7 @@ const imageRoute = require('./api/image');
const attributesRoute = require('./api/attributes'); const attributesRoute = require('./api/attributes');
const scriptRoute = require('./api/script'); const scriptRoute = require('./api/script');
const senderRoute = require('./api/sender'); const senderRoute = require('./api/sender');
const attachmentsRoute = require('./api/attachments');
function register(app) { function register(app) {
app.use('/', indexRoute); app.use('/', indexRoute);
@@ -61,6 +62,7 @@ function register(app) {
app.use('/api/images', imageRoute); app.use('/api/images', imageRoute);
app.use('/api/script', scriptRoute); app.use('/api/script', scriptRoute);
app.use('/api/sender', senderRoute); app.use('/api/sender', senderRoute);
app.use('/api/attachments', attachmentsRoute);
} }
module.exports = { module.exports = {

View File

@@ -1,4 +1,4 @@
<form id="weight-form" style="display: flex; width: 500px; justify-content: space-around; align-items: flex-end;"> <form id="weight-form" style="display: flex; width: 700px; justify-content: space-around; align-items: flex-end;">
<div> <div>
<label for="weight-date">Date</label> <label for="weight-date">Date</label>
<input type="text" id="weight-date" class="form-control" style="width: 150px; text-align: center;" /> <input type="text" id="weight-date" class="form-control" style="width: 150px; text-align: center;" />
@@ -7,6 +7,10 @@
<label for="weight">Weight</label> <label for="weight">Weight</label>
<input type="number" id="weight" value="80.0" step="0.1" class="form-control" style="text-align: center; width: 100px;" /> <input type="number" id="weight" value="80.0" step="0.1" class="form-control" style="text-align: center; width: 100px;" />
</div> </div>
<div>
<label for="comment">Comment</label>
<input type="text" id="comment" class="form-control" style="width: 200px;" />
</div>
<button type="submit" class="btn btn-primary">Add</button> <button type="submit" class="btn btn-primary">Add</button>
</form> </form>
@@ -16,84 +20,127 @@
<canvas id="canvas"></canvas> <canvas id="canvas"></canvas>
<script> <script>
(async function() { (async function() {
const dateEl = $("#weight-date"); const $form = $("#weight-form");
const weightEl = $("#weight"); const $date = $("#weight-date");
const $weight = $("#weight");
const $comment = $("#comment");
let chart;
dateEl.datepicker(); $date.datepicker();
dateEl.datepicker('option', 'dateFormat', 'yy-mm-dd'); $date.datepicker('option', 'dateFormat', 'yy-mm-dd');
dateEl.datepicker('setDate', new Date()); $date.datepicker('setDate', new Date());
async function saveWeight() { async function saveWeight() {
await server.exec([dateEl.val(), weightEl.val()], async (date, weight) => { await server.exec([$date.val(), parseFloat($weight.val()), $comment.val()], async (date, weight, comment) => {
const dataNote = await this.getNoteWithAttribute('date_data', date); const dataNote = await this.getNoteWithAttribute('date_data', date);
if (dataNote) { if (dataNote) {
dataNote.jsonContent.weight = weight; dataNote.jsonContent.weight = weight;
await this.updateEntity(dataNote); if (comment) {
} dataNote.jsonContent.weight_comment = comment;
else {
const parentNoteId = await this.getDateNoteId(date);
const jsonContent = { weight: weight };
await this.createNote(parentNoteId, 'data', jsonContent, {
json: true,
attributes: {
date_data: date,
hide_in_autocomplete: null
} }
});
}
});
showMessage("Weight has been saved"); await this.updateEntity(dataNote);
}
else {
const parentNoteId = await this.getDateNoteId(date);
const jsonContent = { weight: weight };
if (comment) {
jsonContent.weight_comment = comment;
}
await this.createNote(parentNoteId, 'data', jsonContent, {
json: true,
attributes: {
date_data: date,
hide_in_autocomplete: null
}
});
}
});
showMessage("Weight has been saved");
chart.data = await getData();
chart.update();
}
async function drawChart() {
const data = await getData();
const ctx = $("#canvas")[0].getContext("2d");
chart = new Chart(ctx, {
type: 'line',
data: data,
options: {
tooltips: {
enabled: true,
mode: 'single',
callbacks: {
label: function (tooltipItem, data) {
const multistringText = [tooltipItem.yLabel];
const comment = data.comments[tooltipItem['index']];
if (comment) {
multistringText.push(comment);
}
return multistringText;
}
}
},
}
});
}
async function getData() {
const data = await server.exec([], async () => {
const notes = await this.getNotesWithAttribute('date_data');
const data = [];
for (const note of notes) {
const dateAttr = await note.getAttribute('date_data');
data.push({
date: dateAttr.value,
weight: note.jsonContent.weight,
comment: note.jsonContent.weight_comment
});
}
data.sort((a, b) => a.date < b.date ? -1 : +1);
return data;
});
const datasets = [{
label: "Weight",
backgroundColor: 'red',
borderColor: 'red',
data: data.map(row => row.weight),
fill: false
}];
const labels = data.map(row => row.date);
const comments = data.map(row => row.comment);
return {
labels: labels,
datasets: datasets,
comments: comments
};
}
$form.submit(event => {
saveWeight();
event.preventDefault();
});
drawChart(); drawChart();
} })();
async function drawChart() {
const data = await server.exec([], async () => {
const notes = await this.getNotesWithAttribute('date_data');
const data = [];
for (const note of notes) {
const dateAttr = await note.getAttribute('date_data');
data.push({
date: dateAttr.value,
weight: note.jsonContent.weight
});
}
data.sort((a, b) => a.date < b.date ? -1 : +1);
return data;
});
const ctx = $("#canvas")[0].getContext("2d");
new Chart(ctx, {
type: 'line',
data: {
labels: data.map(row => row.date),
datasets: [{
label: "Weight",
backgroundColor: 'red',
borderColor: 'red',
data: data.map(row => row.weight),
fill: false
}]
}
});
}
$("#weight-form").submit(event => {
saveWeight();
event.preventDefault();
});
drawChart();
})();
</script> </script>

View File

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

View File

@@ -13,7 +13,7 @@ const BUILTIN_ATTRIBUTES = [
]; ];
async function getNoteAttributeMap(noteId) { async function getNoteAttributeMap(noteId) {
return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ?`, [noteId]); return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ? AND isDeleted = 0`, [noteId]);
} }
async function getNoteIdWithAttribute(name, value) { async function getNoteIdWithAttribute(name, value) {
@@ -52,7 +52,11 @@ async function getNoteIdsWithAttribute(name) {
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.isDeleted = 0`, [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) { async function createAttribute(noteId, name, value = "", sourceId = null) {
if (value === null || value === undefined) {
value = "";
}
const now = utils.nowDate(); const now = utils.nowDate();
const attributeId = utils.newAttributeId(); const attributeId = utils.newAttributeId();

View File

@@ -214,7 +214,7 @@ async function runAllChecks() {
FROM FROM
notes notes
WHERE WHERE
type != 'text' AND type != 'code' AND type != 'render'`, type != 'text' AND type != 'code' AND type != 'render' AND type != 'file'`,
"Note has invalid type", errorList); "Note has invalid type", errorList);
await runSyncRowChecks("notes", "noteId", errorList); await runSyncRowChecks("notes", "noteId", errorList);

View File

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

View File

@@ -19,7 +19,14 @@ async function executeScript(dataKey, script, params) {
} }
function getParams(params) { function getParams(params) {
return params.map(p => JSON.stringify(p)).join(","); return params.map(p => {
if (typeof p === "string" && p.startsWith("!@#Function: ")) {
return p.substr(13);
}
else {
return JSON.stringify(p);
}
}).join(",");
} }
module.exports = { module.exports = {

View File

@@ -204,6 +204,8 @@ async function pushEntity(sync, syncContext) {
if (sync.entityName === 'notes') { if (sync.entityName === 'notes') {
entity = await sql.getRow('SELECT * FROM notes WHERE noteId = ?', [sync.entityId]); entity = await sql.getRow('SELECT * FROM notes WHERE noteId = ?', [sync.entityId]);
serializeNoteContentBuffer(entity);
} }
else if (sync.entityName === 'note_tree') { else if (sync.entityName === 'note_tree') {
entity = await sql.getRow('SELECT * FROM note_tree WHERE noteTreeId = ?', [sync.entityId]); entity = await sql.getRow('SELECT * FROM note_tree WHERE noteTreeId = ?', [sync.entityId]);
@@ -258,6 +260,12 @@ async function pushEntity(sync, syncContext) {
await syncRequest(syncContext, 'PUT', '/api/sync/' + sync.entityName, payload); await syncRequest(syncContext, 'PUT', '/api/sync/' + sync.entityName, payload);
} }
function serializeNoteContentBuffer(note) {
if (note.type === 'file') {
note.content = note.content.toString("binary");
}
}
async function checkContentHash(syncContext) { async function checkContentHash(syncContext) {
const resp = await syncRequest(syncContext, 'GET', '/api/sync/check'); const resp = await syncRequest(syncContext, 'GET', '/api/sync/check');
@@ -350,5 +358,6 @@ sql.dbReady.then(() => {
}); });
module.exports = { module.exports = {
sync sync,
serializeNoteContentBuffer
}; };

View File

@@ -1,10 +1,17 @@
const sql = require('./sql'); const sql = require('./sql');
const log = require('./log'); const log = require('./log');
const eventLog = require('./event_log'); const eventLog = require('./event_log');
const notes = require('./notes');
const sync_table = require('./sync_table'); const sync_table = require('./sync_table');
function deserializeNoteContentBuffer(note) {
if (note.type === 'file') {
note.content = new Buffer(note.content, 'binary');
}
}
async function updateNote(entity, sourceId) { async function updateNote(entity, sourceId) {
deserializeNoteContentBuffer(entity);
const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]); const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]);
if (!origNote || origNote.dateModified <= entity.dateModified) { if (!origNote || origNote.dateModified <= entity.dateModified) {

View File

@@ -105,7 +105,7 @@
onclick="noteEditor.executeCurrentNote()">Execute <kbd>Ctrl+Enter</kbd></button> onclick="noteEditor.executeCurrentNote()">Execute <kbd>Ctrl+Enter</kbd></button>
<div class="dropdown" id="note-type"> <div class="dropdown" id="note-type">
<button id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm"> <button data-bind="disable: isDisabled()" id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm">
Type: <span data-bind="text: typeString()"></span> Type: <span data-bind="text: typeString()"></span>
<span class="caret"></span> <span class="caret"></span>
</button> </button>
@@ -130,6 +130,7 @@
<li><a onclick="noteHistory.showCurrentNoteHistory();"><kbd>Alt+H</kbd> History</a></li> <li><a onclick="noteHistory.showCurrentNoteHistory();"><kbd>Alt+H</kbd> History</a></li>
<li><a onclick="attributesDialog.showDialog();"><kbd>Alt+A</kbd> Attributes</a></li> <li><a onclick="attributesDialog.showDialog();"><kbd>Alt+A</kbd> Attributes</a></li>
<li><a onclick="noteSource.showDialog();"><kbd>Ctrl+U</kbd> HTML source</a></li> <li><a onclick="noteSource.showDialog();"><kbd>Ctrl+U</kbd> HTML source</a></li>
<li><a onclick="uploadAttachment();">Upload attachment</a></li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -141,6 +142,32 @@
<div id="note-detail-code"></div> <div id="note-detail-code"></div>
<div id="note-detail-render"></div> <div id="note-detail-render"></div>
<div id="note-detail-attachment">
<table id="attachment-table">
<tr>
<th>File name:</th>
<td id="attachment-filename"></td>
</tr>
<tr>
<th>File type:</th>
<td id="attachment-filetype"></td>
</tr>
<tr>
<th>File size:</th>
<td id="attachment-filesize"></td>
</tr>
<tr>
<td>
<button id="attachment-download" class="btn btn-primary" type="button">Download</button>
&nbsp;
<button id="attachment-open" class="btn btn-primary" type="button">Open</button>
</td>
</tr>
</table>
</div>
<input type="file" id="file-upload" style="display: none" />
</div> </div>
<div id="attribute-list"> <div id="attribute-list">
@@ -449,8 +476,6 @@
<link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet"> <link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet">
<script src="libraries/fancytree/jquery.fancytree-all.min.js"></script> <script src="libraries/fancytree/jquery.fancytree-all.min.js"></script>
<script src="libraries/ckeditor/ckeditor.js"></script>
<script src="libraries/jquery.hotkeys.js"></script> <script src="libraries/jquery.hotkeys.js"></script>
<script src="libraries/jquery.fancytree.hotkeys.js"></script> <script src="libraries/jquery.fancytree.hotkeys.js"></script>
@@ -458,15 +483,6 @@
<script src="libraries/knockout.min.js"></script> <script src="libraries/knockout.min.js"></script>
<script src="libraries/codemirror/codemirror.js"></script>
<link rel="stylesheet" href="libraries/codemirror/codemirror.css">
<script src="libraries/codemirror/addon/mode/loadmode.js"></script>
<script src="libraries/codemirror/addon/fold/xml-fold.js"></script>
<script src="libraries/codemirror/addon/edit/matchbrackets.js"></script>
<script src="libraries/codemirror/addon/edit/matchtags.js"></script>
<script src="libraries/codemirror/addon/search/match-highlighter.js"></script>
<script src="libraries/codemirror/mode/meta.js"></script>
<link href="stylesheets/style.css" rel="stylesheet"> <link href="stylesheets/style.css" rel="stylesheet">
<script src="javascripts/utils.js"></script> <script src="javascripts/utils.js"></script>