From f9290562fa14c57aa8fee541b34fe97e21c81759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20Schl=C3=BCter?= Date: Wed, 30 Jan 2019 18:25:35 +0100 Subject: [PATCH] Add rudimentary keyboard shortcuts; closes #41 --- package-lock.json | 96 +++++++++++-------- package.json | 2 +- shadow-cljs.edn | 1 + .../components/keyboard_shortcuts/config.cljs | 19 ++++ .../components/keyboard_shortcuts/events.cljs | 13 +++ .../components/keyboard_shortcuts/views.cljs | 12 +++ src/cljs/airsonic_ui/core.cljs | 5 +- src/cljs/airsonic_ui/views.cljs | 2 + 8 files changed, 109 insertions(+), 41 deletions(-) create mode 100644 src/cljs/airsonic_ui/components/keyboard_shortcuts/config.cljs create mode 100644 src/cljs/airsonic_ui/components/keyboard_shortcuts/events.cljs create mode 100644 src/cljs/airsonic_ui/components/keyboard_shortcuts/views.cljs diff --git a/package-lock.json b/package-lock.json index d553c8a..db64123 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1352,9 +1352,9 @@ "dev": true }, "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", "dev": true }, "evp_bytestokey": { @@ -1766,7 +1766,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -1787,12 +1788,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1807,17 +1810,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -1934,7 +1940,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -1946,6 +1953,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -1960,6 +1968,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -1967,12 +1976,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -1991,6 +2002,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2071,7 +2083,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2083,6 +2096,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -2168,7 +2182,8 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -2204,6 +2219,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2223,6 +2239,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2266,12 +2283,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -2568,9 +2587,9 @@ } }, "hash.js": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz", - "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", "dev": true, "requires": { "inherits": "^2.0.3", @@ -3649,9 +3668,9 @@ } }, "node-libs-browser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", - "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.0.tgz", + "integrity": "sha512-5MQunG/oyOaBdttrL40dA7bUfPORLRWMUJLQtMg7nluxUvk5XwnLdL9twQHFAjRx/y7mIMkLKT9++qPbbk6BZA==", "dev": true, "requires": { "assert": "^1.1.1", @@ -3661,7 +3680,7 @@ "constants-browserify": "^1.0.0", "crypto-browserify": "^3.11.0", "domain-browser": "^1.1.1", - "events": "^1.0.0", + "events": "^3.0.0", "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", "path-browserify": "0.0.0", @@ -3675,7 +3694,7 @@ "timers-browserify": "^2.0.4", "tty-browserify": "0.0.0", "url": "^0.11.0", - "util": "^0.10.3", + "util": "^0.11.0", "vm-browserify": "0.0.4" }, "dependencies": { @@ -4077,22 +4096,23 @@ } }, "pako": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", - "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.8.tgz", + "integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==", "dev": true }, "parse-asn1": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", - "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.3.tgz", + "integrity": "sha512-VrPoetlz7B/FqjBLD2f5wBVZvsZVLnRUrxVLfRYhGXCODa/NWE4p3Wp+6+aV3ZPL3KM7/OZmxDIwwijD7yuucg==", "dev": true, "requires": { "asn1.js": "^4.0.0", "browserify-aes": "^1.0.0", "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3" + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" } }, "parse-json": { @@ -4814,9 +4834,9 @@ } }, "shadow-cljs": { - "version": "2.7.6", - "resolved": "https://registry.npmjs.org/shadow-cljs/-/shadow-cljs-2.7.6.tgz", - "integrity": "sha512-hk9dtt3mLkLQzu2YJG+T2/8YyevRNYtGZTGjTrGCUzjLaqKHJInJELY16vU2W17Kq/u9tCsPV0Y+bbnHRv52uw==", + "version": "2.7.21", + "resolved": "https://registry.npmjs.org/shadow-cljs/-/shadow-cljs-2.7.21.tgz", + "integrity": "sha512-izl5S11oS+p1i46o481VDFOuT1y1LM2k3j9g3JG04KM7exEr02Q10Sz1m5yETM/MkyDxqFGhZWpMfJmCZrOILw==", "dev": true, "requires": { "mkdirp": "^0.5.1", @@ -5221,9 +5241,9 @@ } }, "stream-browserify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", - "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", "dev": true, "requires": { "inherits": "~2.0.1", @@ -5680,9 +5700,9 @@ } }, "util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", "dev": true, "requires": { "inherits": "2.0.3" diff --git a/package.json b/package.json index 418f183..19582b1 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,6 @@ "react-flip-move": "^3.0.3", "react-highlight.js": "^1.0.7", "sass": "^1.15.1", - "shadow-cljs": "^2.7.6" + "shadow-cljs": "^2.7.21" } } diff --git a/shadow-cljs.edn b/shadow-cljs.edn index 0f8533d..6d36d43 100644 --- a/shadow-cljs.edn +++ b/shadow-cljs.edn @@ -5,6 +5,7 @@ :dependencies [[reagent "0.8.0"] [re-frame "0.10.6"] + [re-pressed "0.3.0"] [day8.re-frame/http-fx "0.1.6"] [akiroz.re-frame/storage "0.1.2"] [funcool/bide "1.6.0"] diff --git a/src/cljs/airsonic_ui/components/keyboard_shortcuts/config.cljs b/src/cljs/airsonic_ui/components/keyboard_shortcuts/config.cljs new file mode 100644 index 0000000..3e51c29 --- /dev/null +++ b/src/cljs/airsonic_ui/components/keyboard_shortcuts/config.cljs @@ -0,0 +1,19 @@ +(ns airsonic-ui.components.keyboard-shortcuts.config) + +;; this keymap has the following structure: +;; [[readable-key readable-description event-vector event-keys] +;; ...] + +(def keymap + [["Space" "Toggle play / pause" + [:audio-player/toggle-play-pause] + [{:keyCode 32}]] + ["←" "Previous song" + [:audio-player/previous-song] + [{:keyCode 37}]] + ["→" "Next song" + [:audio-player/next-song] + [{:keyCode 39}]] + ["?" "Show / hide keyboard shortcut help" + [:bulma.modal.events/toggle :keyboard-shortcuts-help] + [{:keyCode 63}]]]) diff --git a/src/cljs/airsonic_ui/components/keyboard_shortcuts/events.cljs b/src/cljs/airsonic_ui/components/keyboard_shortcuts/events.cljs new file mode 100644 index 0000000..e3997ee --- /dev/null +++ b/src/cljs/airsonic_ui/components/keyboard_shortcuts/events.cljs @@ -0,0 +1,13 @@ +(ns airsonic-ui.components.keyboard-shortcuts.events + (:require [re-frame.core :as rf] + [re-pressed.core :as rp] + [airsonic-ui.components.keyboard-shortcuts.config :as config])) + +(rf/reg-event-fx + ::init-shortcuts + (fn [] + (let [event-keys (map (juxt #(nth % 2) #(nth % 3)) config/keymap) + prevent-default-keys (mapcat last event-keys)] + {:dispatch-n [[::rp/add-keyboard-event-listener "keydown"] + [::rp/set-keydown-rules {:event-keys event-keys + :prevent-default-keys prevent-default-keys}]]}))) diff --git a/src/cljs/airsonic_ui/components/keyboard_shortcuts/views.cljs b/src/cljs/airsonic_ui/components/keyboard_shortcuts/views.cljs new file mode 100644 index 0000000..9d1d95a --- /dev/null +++ b/src/cljs/airsonic_ui/components/keyboard_shortcuts/views.cljs @@ -0,0 +1,12 @@ +(ns airsonic-ui.components.keyboard-shortcuts.views + (:require [bulma.modal.views :as bulma] + [airsonic-ui.components.keyboard-shortcuts.config :as config])) + +(defn help-modal [] + [bulma/modal-card {:title "Keyboard Shortcuts" + :modal-id :keyboard-shortcuts-help} + [:table.table.is-hoverable.is-fullwidth + [:thead [:tr [:th "Key"] [:th "Function"]]] + [:tbody + (for [[idx [k desc]] (map-indexed vector config/keymap)] + ^{:key idx} [:tr [:td>code k] [:td desc]])]]]) diff --git a/src/cljs/airsonic_ui/core.cljs b/src/cljs/airsonic_ui/core.cljs index 924a7c9..9ee6cd8 100644 --- a/src/cljs/airsonic_ui/core.cljs +++ b/src/cljs/airsonic_ui/core.cljs @@ -11,6 +11,7 @@ [airsonic-ui.api.events] [airsonic-ui.api.subs] [airsonic-ui.components.audio-player.events] + [airsonic-ui.components.keyboard-shortcuts.events :as keyboard] [airsonic-ui.components.library.subs] [airsonic-ui.components.search.events] [airsonic-ui.components.search.subs] @@ -28,8 +29,8 @@ (reagent/render [views/main-panel] (.getElementById js/document "app"))) (defn ^:export init [] - (storage/reg-co-fx! :airsonic-ui {:fx :store - :cofx :store}) + (storage/reg-co-fx! :airsonic-ui {:fx :store, :cofx :store}) (rf/dispatch-sync [::events/initialize-app]) + (rf/dispatch [::keyboard/init-shortcuts]) (dev-setup) (mount-root)) diff --git a/src/cljs/airsonic_ui/views.cljs b/src/cljs/airsonic_ui/views.cljs index 04a7234..dbba2ae 100644 --- a/src/cljs/airsonic_ui/views.cljs +++ b/src/cljs/airsonic_ui/views.cljs @@ -19,6 +19,7 @@ [airsonic-ui.components.bangpow.views :refer [not-found]] [airsonic-ui.components.collection.views :as collection] [airsonic-ui.components.current-queue.views :refer [current-queue]] + [airsonic-ui.components.keyboard-shortcuts.views :as keyboard] [airsonic-ui.components.library.views :as library] [airsonic-ui.components.podcast.views :as podcast] [airsonic-ui.components.search.views :as search])) @@ -130,6 +131,7 @@ [route-id :as route] @(subscribe [:routes/current-route])] [(add-classes :div route-id) [notification-list notifications] + [keyboard/help-modal] (if is-booting? [:div.app-loading>div.loader] [:div