diff --git a/package-lock.json b/package-lock.json index a07c3605a..fd5df94ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3112,9 +3112,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "ejs": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.2.tgz", - "integrity": "sha512-rHGwtpl67oih3xAHbZlpw5rQAt+YV1mSCu2fUZ9XNrfaGEhom7E+AUiMci+ByP4aSfuAWx7hE0BPuJLMrpXwOw==" + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.3.tgz", + "integrity": "sha512-NtMNsdpaCF23gvHItgT37gzrpzckzs7KB7mg+YH1GMSG/5iZRq1BeWzAhEAJVagfM7nCQDnh/C51j/L2qjZmnA==" }, "electron": { "version": "6.0.12", diff --git a/package.json b/package.json index 33abff9ea..002cc9bd8 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "csurf": "1.10.0", "dayjs": "1.8.17", "debug": "4.1.1", - "ejs": "2.7.2", + "ejs": "2.7.3", "electron-debug": "3.0.1", "electron-dl": "1.14.0", "electron-find": "1.0.6", diff --git a/src/public/javascripts/services/entrypoints.js b/src/public/javascripts/services/entrypoints.js index d0142e55d..e4cd13471 100644 --- a/src/public/javascripts/services/entrypoints.js +++ b/src/public/javascripts/services/entrypoints.js @@ -4,6 +4,7 @@ import zoomService from "./zoom.js"; import protectedSessionService from "./protected_session.js"; import searchNotesService from "./search_notes.js"; import treeService from "./tree.js"; +import server from "./server.js"; const NOTE_REVISIONS = "../dialogs/note_revisions.js"; const OPTIONS = "../dialogs/options.js"; @@ -208,6 +209,43 @@ function registerEntrypoints() { } +class KeyboardAction { + constructor(params) { + /** @property {string} */ + this.optionName = params.optionName; + /** @property {string[]} */ + this.defaultShortcuts = Array.isArray(params.defaultShortcuts) ? params.defaultShortcuts : [params.defaultShortcuts]; + /** @property {string[]} */ + this.activeShortcuts = this.defaultShortcuts.slice(); + /** @property {string} */ + this.description = params.description; + } + + addShortcut(shortcut) { + this.activeShortcuts.push(shortcut); + } + + /** + * @param {string|string[]} shortcuts + */ + replaceShortcuts(shortcuts) { + this.activeShortcuts = Array.isArray(shortcuts) ? shortcuts : [shortcuts]; + } + + /** @return {KeyboardAction[]} */ + static get allActions() { + return Object.keys(KeyboardAction) + .map(key => KeyboardAction[key]) + .filter(obj => obj instanceof KeyboardAction); + } +} + +server.get('keyboard-actions').then(actions => { + for (const action of actions) { + + } +}); + export default { registerEntrypoints } \ No newline at end of file diff --git a/src/public/javascripts/services/options.js b/src/public/javascripts/services/options.js index c67190710..67059f1c0 100644 --- a/src/public/javascripts/services/options.js +++ b/src/public/javascripts/services/options.js @@ -13,6 +13,10 @@ class Options { return this.arr[key]; } + getNames() { + return Object.keys(this.arr); + } + getJson(key) { try { return JSON.parse(this.arr[key]); diff --git a/src/public/javascripts/services/sidebar.js b/src/public/javascripts/services/sidebar.js index 29a84f7d8..5dbbf8f4d 100644 --- a/src/public/javascripts/services/sidebar.js +++ b/src/public/javascripts/services/sidebar.js @@ -58,7 +58,7 @@ class Sidebar { import("../widgets/note_revisions.js"), import("../widgets/attributes.js"), import("../widgets/what_links_here.js"), - import("../widgets/similar_notes.js"), + import("../widgets/similar-notes.js"), import("../widgets/edited_notes.js"), import("../widgets/calendar.js") ])).map(m => m.default); diff --git a/src/public/javascripts/widgets/similar_notes.js b/src/public/javascripts/widgets/similar_notes.js index 98a43e233..e4e338032 100644 --- a/src/public/javascripts/widgets/similar_notes.js +++ b/src/public/javascripts/widgets/similar_notes.js @@ -18,7 +18,7 @@ class SimilarNotesWidget extends StandardWidget { // remember which title was when we found the similar notes this.title = this.ctx.note.title; - const similarNotes = await server.get('similar_notes/' + this.ctx.note.noteId); + const similarNotes = await server.get('similar-notes/' + this.ctx.note.noteId); if (similarNotes.length === 0) { this.$body.text("No similar notes found ..."); diff --git a/src/routes/api/keys.js b/src/routes/api/keys.js new file mode 100644 index 000000000..8d81c0842 --- /dev/null +++ b/src/routes/api/keys.js @@ -0,0 +1,11 @@ +"use strict"; + +const keyboardActions = require('../../services/keyboard_actions'); + +async function getKeyboardActions() { + return await keyboardActions.getKeyboardActions(); +} + +module.exports = { + getKeyboardActions +}; \ No newline at end of file diff --git a/src/routes/api/options.js b/src/routes/api/options.js index 7ee49f476..09ee01532 100644 --- a/src/routes/api/options.js +++ b/src/routes/api/options.js @@ -5,7 +5,7 @@ const log = require('../../services/log'); const attributes = require('../../services/attributes'); // options allowed to be updated directly in options dialog -const ALLOWED_OPTIONS = [ +const ALLOWED_OPTIONS = new Set([ 'protectedSessionTimeout', 'noteRevisionSnapshotTimeInterval', 'zoomFactor', @@ -37,23 +37,32 @@ const ALLOWED_OPTIONS = [ 'spellCheckLanguageCode', 'imageMaxWidthHeight', 'imageJpegQuality' -]; +]); async function getOptions() { - return await optionService.getOptionsMap(ALLOWED_OPTIONS); + const optionMap = await optionService.getOptionsMap(); + const resultMap = {}; + + for (const optionName in optionMap) { + if (isAllowed(optionName)) { + resultMap[optionName] = optionMap[optionName]; + } + } + + return resultMap; } async function updateOption(req) { const {name, value} = req.params; - if (!update(name, value)) { + if (!await update(name, value)) { return [400, "not allowed option to change"]; } } async function updateOptions(req) { for (const optionName in req.body) { - if (!update(optionName, req.body[optionName])) { + if (!await update(optionName, req.body[optionName])) { // this should be improved // it should return 400 instead of current 500, but at least it now rollbacks transaction throw new Error(`${optionName} is not allowed to change`); @@ -62,7 +71,7 @@ async function updateOptions(req) { } async function update(name, value) { - if (!ALLOWED_OPTIONS.includes(name)) { + if (!isAllowed(name)) { return false; } @@ -97,6 +106,10 @@ async function getUserThemes() { return ret; } +function isAllowed(name) { + return ALLOWED_OPTIONS.has(name) || name.startsWith("keyboardShortcuts"); +} + module.exports = { getOptions, updateOption, diff --git a/src/routes/routes.js b/src/routes/routes.js index 4f71da1a0..fb33589d0 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -33,7 +33,8 @@ const searchRoute = require('./api/search'); const dateNotesRoute = require('./api/date_notes'); const linkMapRoute = require('./api/link_map'); const clipperRoute = require('./api/clipper'); -const similarNotesRoute = require('./api/similar_notes'); +const similarNotesRoute = require('./api/similar-notes'); +const keysRoute = require('./api/keys'); const log = require('../services/log'); const express = require('express'); @@ -242,7 +243,9 @@ function register(app) { route(POST, '/api/clipper/notes', clipperMiddleware, clipperRoute.createNote, apiResultHandler); route(POST, '/api/clipper/open/:noteId', clipperMiddleware, clipperRoute.openNote, apiResultHandler); - apiRoute(GET, '/api/similar_notes/:noteId', similarNotesRoute.getSimilarNotes); + apiRoute(GET, '/api/similar-notes/:noteId', similarNotesRoute.getSimilarNotes); + + apiRoute(GET, '/api/keyboard-actions', keysRoute.getKeyboardActions); app.use('', router); } diff --git a/src/services/keyboard_actions.js b/src/services/keyboard_actions.js index 6a8d345b4..08e5d63fc 100644 --- a/src/services/keyboard_actions.js +++ b/src/services/keyboard_actions.js @@ -1,157 +1,191 @@ +"use strict"; + +const optionService = require('./options'); +const log = require('./log'); + const ELECTRON = "electron"; -const KEYBOARD_ACTIONS = [ +const DEFAULT_KEYBOARD_ACTIONS = [ { - optionName: "JumpToNote", + actionName: "JumpToNote", defaultShortcuts: ["mod+j"], description: 'Open "Jump to note" dialog' }, { - optionName: "MarkdownToHTML", + actionName: "MarkdownToHTML", defaultShortcuts: ["mod+return"] }, { - optionName: "NewTab", + actionName: "NewTab", defaultShortcuts: ["mod+t"], only: ELECTRON }, { - optionName: "CloseTab", + actionName: "CloseTab", defaultShortcuts: ["mod+w"], only: ELECTRON }, { - optionName: "NextTab", + actionName: "NextTab", defaultShortcuts: ["mod+tab"], only: ELECTRON }, { - optionName: "PreviousTab", + actionName: "PreviousTab", defaultShortcuts: ["mod+shift+tab"], only: ELECTRON }, { - optionName: "CreateNoteAfter", + actionName: "CreateNoteAfter", defaultShortcuts: ["mod+o"] }, { - optionName: "CreateNoteInto", + actionName: "CreateNoteInto", defaultShortcuts: ["mod+p"] }, { - optionName: "ScrollToActiveNote", + actionName: "ScrollToActiveNote", defaultShortcuts: ["mod+."] }, { - optionName: "CollapseTree", + actionName: "CollapseTree", defaultShortcuts: ["alt+c"] }, { - optionName: "RunSQL", + actionName: "RunSQL", defaultShortcuts: ["mod+return"] }, { - optionName: "FocusNote", + actionName: "FocusNote", defaultShortcuts: ["return"] }, { - optionName: "RunCurrentNote", + actionName: "RunCurrentNote", defaultShortcuts: ["mod+return"] }, { - optionName: "ClipboardCopy", + actionName: "ClipboardCopy", defaultShortcuts: ["mod+c"] }, { - optionName: "ClipboardPaste", + actionName: "ClipboardPaste", defaultShortcuts: ["mod+v"] }, { - optionName: "ClipboardCut", + actionName: "ClipboardCut", defaultShortcuts: ["mod+x"] }, { - optionName: "SelectAllNotesInParent", + actionName: "SelectAllNotesInParent", defaultShortcuts: ["mod+a"] }, { - optionName: "Undo", + actionName: "Undo", defaultShortcuts: ["mod+z"] }, { - optionName: "Redo", + actionName: "Redo", defaultShortcuts: ["mod+y"] }, { - optionName: "AddLinkToText", + actionName: "AddLinkToText", defaultShortcuts: ["mod+l"] }, { - optionName: "CloneNotesTo", + actionName: "CloneNotesTo", defaultShortcuts: ["mod+shift+c"] }, { - optionName: "MoveNotesTo", + actionName: "MoveNotesTo", defaultShortcuts: ["mod+shift+c"] }, { - optionName: "SearchNotes", + actionName: "SearchNotes", defaultShortcuts: ["mod+s"] }, { - optionName: "ShowAttributes", + actionName: "ShowAttributes", defaultShortcuts: ["alt+a"] }, { - optionName: "ShowHelp", + actionName: "ShowHelp", defaultShortcuts: ["f1"] }, { - optionName: "OpenSQLConsole", + actionName: "OpenSQLConsole", defaultShortcuts: ["alt+o"] }, { - optionName: "BackInNoteHistory", + actionName: "BackInNoteHistory", defaultShortcuts: ["alt+left"] }, { - optionName: "ForwardInNoteHistory", + actionName: "ForwardInNoteHistory", defaultShortcuts: ["alt+right"] }, { - optionName: "ToggleZenMode", + actionName: "ToggleZenMode", defaultShortcuts: ["alt+m"] }, { - optionName: "InsertDateTime", + actionName: "InsertDateTime", defaultShortcuts: ["alt+t"] }, { - optionName: "ReloadApp", + actionName: "ReloadApp", defaultShortcuts: ["f5", "mod+r"] }, { - optionName: "OpenDevTools", + actionName: "OpenDevTools", defaultShortcuts: ["mod+shift+i"] }, { - optionName: "FindInText", + actionName: "FindInText", defaultShortcuts: ["mod+f"] }, { - optionName: "ToggleFullscreen", + actionName: "ToggleFullscreen", defaultShortcuts: ["f11"] }, { - optionName: "ZoomOut", + actionName: "ZoomOut", defaultShortcuts: ["mod+-"] }, { - optionName: "ZoomIn", + actionName: "ZoomIn", defaultShortcuts: ["mod+="] } ]; +async function getKeyboardActions() { + const actions = JSON.parse(JSON.stringify(DEFAULT_KEYBOARD_ACTIONS)); + + for (const action of actions) { + action.effectiveShortcuts = action.defaultShortcuts.slice(); + } + + for (const option of await optionService.getOptions()) { + if (option.name.startsWith('keyboardShortcuts')) { + const actionName = option.name.substr(17); + + const action = actions.find(ea => ea.actionName === actionName); + + if (action) { + try { + action.effectiveShortcuts = JSON.parse(option.value); + } + catch (e) { + log.error(`Could not parse shortcuts for action ${actionName}`); + } + } + else { + log.info(`Keyboard action ${actionName} not found.`); + } + } + } +} + module.exports = { - KEYBOARD_ACTIONS + DEFAULT_KEYBOARD_ACTIONS, + getKeyboardActions }; \ No newline at end of file diff --git a/src/services/options.js b/src/services/options.js index ed5d2b1c3..00ca87399 100644 --- a/src/services/options.js +++ b/src/services/options.js @@ -61,18 +61,12 @@ async function createOption(name, value, isSynced) { }).save(); } -async function getOptions(allowedOptions) { - let options = await require('./repository').getEntities("SELECT * FROM options ORDER BY name"); - - if (allowedOptions) { - options = options.filter(opt => allowedOptions.includes(opt.name)); - } - - return options; +async function getOptions() { + return await require('./repository').getEntities("SELECT * FROM options ORDER BY name"); } -async function getOptionsMap(allowedOptions) { - const options = await getOptions(allowedOptions); +async function getOptionsMap() { + const options = await getOptions(); return utils.toObject(options, opt => [opt.name, opt.value]); } diff --git a/src/services/options_init.js b/src/services/options_init.js index 26efc4ca1..0e8c54860 100644 --- a/src/services/options_init.js +++ b/src/services/options_init.js @@ -100,7 +100,7 @@ async function initStartupOptions() { } function getKeyboardDefaultOptions() { - return keyboardActions.KEYBOARD_ACTIONS.map(ka => { + return keyboardActions.DEFAULT_KEYBOARD_ACTIONS.map(ka => { return { name: "keyboardShortcuts" + ka.optionName, value: JSON.stringify(ka.defaultShortcuts),