1
0
Fork 0
mirror of https://github.com/heyarne/airsonic-ui.git synced 2026-05-06 18:33:38 +02:00

Add keyboard shortcuts (#43)

* Use rf instead of re-frame

* Add bulma modal component

* Add option to toggle a modal

* Add rudimentary keyboard shortcuts; closes #41
This commit is contained in:
Arne Schlüter 2019-01-30 18:35:08 +01:00 committed by GitHub
commit 149fd07566
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 291 additions and 101 deletions

96
package-lock.json generated
View file

@ -1352,9 +1352,9 @@
"dev": true "dev": true
}, },
"events": { "events": {
"version": "1.1.1", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz",
"integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==",
"dev": true "dev": true
}, },
"evp_bytestokey": { "evp_bytestokey": {
@ -1766,7 +1766,8 @@
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@ -1787,12 +1788,14 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -1807,17 +1810,20 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -1934,7 +1940,8 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -1946,6 +1953,7 @@
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -1960,6 +1968,7 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -1967,12 +1976,14 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.2.4", "version": "2.2.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.1", "safe-buffer": "^5.1.1",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -1991,6 +2002,7 @@
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -2071,7 +2083,8 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -2083,6 +2096,7 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -2168,7 +2182,8 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.1", "version": "5.1.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@ -2204,6 +2219,7 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -2223,6 +2239,7 @@
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@ -2266,12 +2283,14 @@
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.2", "version": "3.0.2",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
} }
} }
}, },
@ -2568,9 +2587,9 @@
} }
}, },
"hash.js": { "hash.js": {
"version": "1.1.5", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"dev": true, "dev": true,
"requires": { "requires": {
"inherits": "^2.0.3", "inherits": "^2.0.3",
@ -3649,9 +3668,9 @@
} }
}, },
"node-libs-browser": { "node-libs-browser": {
"version": "2.1.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.0.tgz",
"integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", "integrity": "sha512-5MQunG/oyOaBdttrL40dA7bUfPORLRWMUJLQtMg7nluxUvk5XwnLdL9twQHFAjRx/y7mIMkLKT9++qPbbk6BZA==",
"dev": true, "dev": true,
"requires": { "requires": {
"assert": "^1.1.1", "assert": "^1.1.1",
@ -3661,7 +3680,7 @@
"constants-browserify": "^1.0.0", "constants-browserify": "^1.0.0",
"crypto-browserify": "^3.11.0", "crypto-browserify": "^3.11.0",
"domain-browser": "^1.1.1", "domain-browser": "^1.1.1",
"events": "^1.0.0", "events": "^3.0.0",
"https-browserify": "^1.0.0", "https-browserify": "^1.0.0",
"os-browserify": "^0.3.0", "os-browserify": "^0.3.0",
"path-browserify": "0.0.0", "path-browserify": "0.0.0",
@ -3675,7 +3694,7 @@
"timers-browserify": "^2.0.4", "timers-browserify": "^2.0.4",
"tty-browserify": "0.0.0", "tty-browserify": "0.0.0",
"url": "^0.11.0", "url": "^0.11.0",
"util": "^0.10.3", "util": "^0.11.0",
"vm-browserify": "0.0.4" "vm-browserify": "0.0.4"
}, },
"dependencies": { "dependencies": {
@ -4077,22 +4096,23 @@
} }
}, },
"pako": { "pako": {
"version": "1.0.6", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.8.tgz",
"integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", "integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==",
"dev": true "dev": true
}, },
"parse-asn1": { "parse-asn1": {
"version": "5.1.1", "version": "5.1.3",
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.3.tgz",
"integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", "integrity": "sha512-VrPoetlz7B/FqjBLD2f5wBVZvsZVLnRUrxVLfRYhGXCODa/NWE4p3Wp+6+aV3ZPL3KM7/OZmxDIwwijD7yuucg==",
"dev": true, "dev": true,
"requires": { "requires": {
"asn1.js": "^4.0.0", "asn1.js": "^4.0.0",
"browserify-aes": "^1.0.0", "browserify-aes": "^1.0.0",
"create-hash": "^1.1.0", "create-hash": "^1.1.0",
"evp_bytestokey": "^1.0.0", "evp_bytestokey": "^1.0.0",
"pbkdf2": "^3.0.3" "pbkdf2": "^3.0.3",
"safe-buffer": "^5.1.1"
} }
}, },
"parse-json": { "parse-json": {
@ -4814,9 +4834,9 @@
} }
}, },
"shadow-cljs": { "shadow-cljs": {
"version": "2.7.6", "version": "2.7.21",
"resolved": "https://registry.npmjs.org/shadow-cljs/-/shadow-cljs-2.7.6.tgz", "resolved": "https://registry.npmjs.org/shadow-cljs/-/shadow-cljs-2.7.21.tgz",
"integrity": "sha512-hk9dtt3mLkLQzu2YJG+T2/8YyevRNYtGZTGjTrGCUzjLaqKHJInJELY16vU2W17Kq/u9tCsPV0Y+bbnHRv52uw==", "integrity": "sha512-izl5S11oS+p1i46o481VDFOuT1y1LM2k3j9g3JG04KM7exEr02Q10Sz1m5yETM/MkyDxqFGhZWpMfJmCZrOILw==",
"dev": true, "dev": true,
"requires": { "requires": {
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
@ -5221,9 +5241,9 @@
} }
}, },
"stream-browserify": { "stream-browserify": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
"integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==",
"dev": true, "dev": true,
"requires": { "requires": {
"inherits": "~2.0.1", "inherits": "~2.0.1",
@ -5680,9 +5700,9 @@
} }
}, },
"util": { "util": {
"version": "0.10.4", "version": "0.11.1",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"inherits": "2.0.3" "inherits": "2.0.3"

View file

@ -43,6 +43,6 @@
"react-flip-move": "^3.0.3", "react-flip-move": "^3.0.3",
"react-highlight.js": "^1.0.7", "react-highlight.js": "^1.0.7",
"sass": "^1.15.1", "sass": "^1.15.1",
"shadow-cljs": "^2.7.6" "shadow-cljs": "^2.7.21"
} }
} }

View file

@ -5,6 +5,7 @@
:dependencies :dependencies
[[reagent "0.8.0"] [[reagent "0.8.0"]
[re-frame "0.10.6"] [re-frame "0.10.6"]
[re-pressed "0.3.0"]
[day8.re-frame/http-fx "0.1.6"] [day8.re-frame/http-fx "0.1.6"]
[akiroz.re-frame/storage "0.1.2"] [akiroz.re-frame/storage "0.1.2"]
[funcool/bide "1.6.0"] [funcool/bide "1.6.0"]

View file

@ -2,7 +2,7 @@
"This namespace contains some JS interop code to interact with an audio player "This namespace contains some JS interop code to interact with an audio player
and receive information about the current playback status so we can use it in and receive information about the current playback status so we can use it in
our re-frame app." our re-frame app."
(:require [re-frame.core :as re-frame] (:require [re-frame.core :as rf]
[airsonic-ui.audio.playlist :as playlist] [airsonic-ui.audio.playlist :as playlist]
[goog.functions :refer [throttle]])) [goog.functions :refer [throttle]]))
@ -29,13 +29,13 @@
(defn attach-listeners! [el] (defn attach-listeners! [el]
(let [emit-audio-update (throttle #(re-frame/dispatch [:audio/update (->status el)]) 16)] (let [emit-audio-update (throttle #(rf/dispatch [:audio/update (->status el)]) 16)]
(doseq [event ["loadstart" "progress" "play" "timeupdate" "pause"]] (doseq [event ["loadstart" "progress" "play" "timeupdate" "pause"]]
(.addEventListener el event emit-audio-update)))) (.addEventListener el event emit-audio-update))))
;; effects to be fired from event handlers ;; effects to be fired from event handlers
(re-frame/reg-fx (rf/reg-fx
:audio/play :audio/play
(fn [stream-url] (fn [stream-url]
(when-not @audio (when-not @audio
@ -45,19 +45,19 @@
(set! (.-src @audio) stream-url) (set! (.-src @audio) stream-url)
(.play @audio))) (.play @audio)))
(re-frame/reg-fx (rf/reg-fx
:audio/pause :audio/pause
(fn [_] (fn [_]
(some-> @audio .pause))) (some-> @audio .pause)))
(re-frame/reg-fx (rf/reg-fx
:audio/stop :audio/stop
(fn [_] (fn [_]
(when-let [audio @audio] (when-let [audio @audio]
(.pause audio) (.pause audio)
(set! (.-currentTime audio) 0)))) (set! (.-currentTime audio) 0))))
(re-frame/reg-fx (rf/reg-fx
:audio/toggle-play-pause :audio/toggle-play-pause
(fn [_] (fn [_]
(if-let [a @audio] (if-let [a @audio]
@ -65,7 +65,7 @@
(.play a) (.play a)
(.pause a))))) (.pause a)))))
(re-frame/reg-fx (rf/reg-fx
:audio/seek :audio/seek
(fn [[percentage duration]] (fn [[percentage duration]]
(set! (. @audio -currentTime) (set! (. @audio -currentTime)
@ -78,16 +78,16 @@
[db _] [db _]
(:audio db)) (:audio db))
(re-frame/reg-sub :audio/summary summary) (rf/reg-sub :audio/summary summary)
(defn playlist (defn playlist
"Lists the complete playlist" "Lists the complete playlist"
[summary _] [summary _]
(:playlist summary)) (:playlist summary))
(re-frame/reg-sub (rf/reg-sub
:audio/playlist :audio/playlist
(fn [_ _] (re-frame/subscribe [:audio/summary])) (fn [_ _] (rf/subscribe [:audio/summary]))
playlist) playlist)
(defn current-song (defn current-song
@ -96,9 +96,9 @@
[playlist _] [playlist _]
(playlist/peek playlist)) (playlist/peek playlist))
(re-frame/reg-sub (rf/reg-sub
:audio/current-song :audio/current-song
(fn [_ _] (re-frame/subscribe [:audio/playlist])) (fn [_ _] (rf/subscribe [:audio/playlist]))
current-song) current-song)
(defn playback-status (defn playback-status
@ -106,9 +106,9 @@
[summary _] [summary _]
(:playback-status summary)) (:playback-status summary))
(re-frame/reg-sub (rf/reg-sub
:audio/playback-status :audio/playback-status
(fn [_ _] (re-frame/subscribe [:audio/summary])) (fn [_ _] (rf/subscribe [:audio/summary]))
playback-status) playback-status)
(defn is-playing? (defn is-playing?
@ -117,7 +117,7 @@
(and (not (:paused? playback-status)) (and (not (:paused? playback-status))
(not (:ended? playback-status)))) (not (:ended? playback-status))))
(re-frame/reg-sub (rf/reg-sub
:audio/is-playing? :audio/is-playing?
(fn [_ _] (re-frame/subscribe [:audio/playback-status])) (fn [_ _] (rf/subscribe [:audio/playback-status]))
is-playing?) is-playing?)

View file

@ -1,9 +1,9 @@
(ns airsonic-ui.components.audio-player.events (ns airsonic-ui.components.audio-player.events
(:require [re-frame.core :as re-frame] (:require [re-frame.core :as rf]
[airsonic-ui.audio.playlist :as playlist] [airsonic-ui.audio.playlist :as playlist]
[airsonic-ui.api.helpers :as api])) [airsonic-ui.api.helpers :as api]))
(re-frame/reg-event-fx (rf/reg-event-fx
; sets up the db, starts to play a song and adds the rest to a playlist ; sets up the db, starts to play a song and adds the rest to a playlist
:audio-player/play-all :audio-player/play-all
(fn [{:keys [db]} [_ songs start-idx]] (fn [{:keys [db]} [_ songs start-idx]]
@ -12,17 +12,17 @@
{:audio/play (api/stream-url (:credentials db) (playlist/peek playlist)) {:audio/play (api/stream-url (:credentials db) (playlist/peek playlist))
:db (assoc-in db [:audio :playlist] playlist)}))) :db (assoc-in db [:audio :playlist] playlist)})))
(re-frame/reg-event-db (rf/reg-event-db
:audio-player/set-playback-mode :audio-player/set-playback-mode
(fn [db [_ playback-mode]] (fn [db [_ playback-mode]]
(update-in db [:audio :playlist] #(playlist/set-playback-mode % playback-mode)))) (update-in db [:audio :playlist] #(playlist/set-playback-mode % playback-mode))))
(re-frame/reg-event-db (rf/reg-event-db
:audio-player/set-repeat-mode :audio-player/set-repeat-mode
(fn [db [_ repeat-mode]] (fn [db [_ repeat-mode]]
(update-in db [:audio :playlist] #(playlist/set-repeat-mode % repeat-mode)))) (update-in db [:audio :playlist] #(playlist/set-repeat-mode % repeat-mode))))
(re-frame/reg-event-fx (rf/reg-event-fx
:audio-player/next-song :audio-player/next-song
(fn [{:keys [db]} _] (fn [{:keys [db]} _]
(let [db (update-in db [:audio :playlist] playlist/next-song) (let [db (update-in db [:audio :playlist] playlist/next-song)
@ -30,7 +30,7 @@
{:db db {:db db
:audio/play (api/stream-url (:credentials db) next)}))) :audio/play (api/stream-url (:credentials db) next)})))
(re-frame/reg-event-fx (rf/reg-event-fx
:audio-player/previous-song :audio-player/previous-song
(fn [{:keys [db]} _] (fn [{:keys [db]} _]
(let [db (update-in db [:audio :playlist] playlist/previous-song) (let [db (update-in db [:audio :playlist] playlist/previous-song)
@ -38,17 +38,17 @@
{:db db {:db db
:audio/play (api/stream-url (:credentials db) prev)}))) :audio/play (api/stream-url (:credentials db) prev)})))
(re-frame/reg-event-db (rf/reg-event-db
:audio-player/enqueue-next :audio-player/enqueue-next
(fn [db [_ song]] (fn [db [_ song]]
(update-in db [:audio :playlist] #(playlist/enqueue-next % song)))) (update-in db [:audio :playlist] #(playlist/enqueue-next % song))))
(re-frame/reg-event-db (rf/reg-event-db
:audio-player/enqueue-last :audio-player/enqueue-last
(fn [db [_ song]] (fn [db [_ song]]
(update-in db [:audio :playlist] #(playlist/enqueue-last % song)))) (update-in db [:audio :playlist] #(playlist/enqueue-last % song))))
(re-frame/reg-event-fx (rf/reg-event-fx
:audio-player/toggle-play-pause :audio-player/toggle-play-pause
(fn [_ _] (fn [_ _]
{:audio/toggle-play-pause nil})) {:audio/toggle-play-pause nil}))
@ -60,9 +60,9 @@
(cond-> {:db (assoc-in db [:audio :playback-status] status)} (cond-> {:db (assoc-in db [:audio :playback-status] status)}
(:ended? status) (assoc :dispatch [:audio-player/next-song]))) (:ended? status) (assoc :dispatch [:audio-player/next-song])))
(re-frame/reg-event-fx :audio/update audio-update) (rf/reg-event-fx :audio/update audio-update)
(re-frame/reg-event-fx (rf/reg-event-fx
:audio-player/seek :audio-player/seek
(fn [{:keys [db]} [_ percentage]] (fn [{:keys [db]} [_ percentage]]
(let [duration (:duration (playlist/peek (get-in db [:audio :playlist])))] (let [duration (:duration (playlist/peek (get-in db [:audio :playlist])))]

View file

@ -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}]]])

View file

@ -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}]]})))

View file

@ -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]])]]])

View file

@ -1,5 +1,5 @@
(ns airsonic-ui.components.library.subs (ns airsonic-ui.components.library.subs
(:require [re-frame.core :as re-frame] (:require [re-frame.core :as rf]
[airsonic-ui.config :as conf])) [airsonic-ui.config :as conf]))
;; first some helper functions to make the structure a bit clearer ;; first some helper functions to make the structure a bit clearer
@ -34,7 +34,7 @@
(map (fn [[k v]] [(inc k) v])) (map (fn [[k v]] [(inc k) v]))
(into (sorted-map)))) (into (sorted-map))))
(re-frame/reg-sub (rf/reg-sub
:library/paginated :library/paginated
:<- [:api/responses-for-endpoint "getAlbumList2"] :<- [:api/responses-for-endpoint "getAlbumList2"]
paginated-library) paginated-library)

View file

@ -1,6 +1,6 @@
(ns airsonic-ui.core (ns airsonic-ui.core
(:require [reagent.core :as reagent] (:require [reagent.core :as reagent]
[re-frame.core :as re-frame] [re-frame.core :as rf]
;; 3rd party effects / coeffects ;; 3rd party effects / coeffects
[day8.re-frame.http-fx] [day8.re-frame.http-fx]
[akiroz.re-frame.storage :as storage] [akiroz.re-frame.storage :as storage]
@ -11,6 +11,7 @@
[airsonic-ui.api.events] [airsonic-ui.api.events]
[airsonic-ui.api.subs] [airsonic-ui.api.subs]
[airsonic-ui.components.audio-player.events] [airsonic-ui.components.audio-player.events]
[airsonic-ui.components.keyboard-shortcuts.events :as keyboard]
[airsonic-ui.components.library.subs] [airsonic-ui.components.library.subs]
[airsonic-ui.components.search.events] [airsonic-ui.components.search.events]
[airsonic-ui.components.search.subs] [airsonic-ui.components.search.subs]
@ -24,12 +25,12 @@
(println "dev mode"))) (println "dev mode")))
(defn mount-root [] (defn mount-root []
(re-frame/clear-subscription-cache!) (rf/clear-subscription-cache!)
(reagent/render [views/main-panel] (.getElementById js/document "app"))) (reagent/render [views/main-panel] (.getElementById js/document "app")))
(defn ^:export init [] (defn ^:export init []
(storage/reg-co-fx! :airsonic-ui {:fx :store (storage/reg-co-fx! :airsonic-ui {:fx :store, :cofx :store})
:cofx :store}) (rf/dispatch-sync [::events/initialize-app])
(re-frame/dispatch-sync [::events/initialize-app]) (rf/dispatch [::keyboard/init-shortcuts])
(dev-setup) (dev-setup)
(mount-root)) (mount-root))

View file

@ -1,11 +1,11 @@
(ns airsonic-ui.events (ns airsonic-ui.events
(:require [re-frame.core :as re-frame] (:require [re-frame.core :as rf]
[ajax.core :as ajax] [ajax.core :as ajax]
[airsonic-ui.routes :as routes] [airsonic-ui.routes :as routes]
[airsonic-ui.db :as db] [airsonic-ui.db :as db]
[airsonic-ui.api.helpers :as api])) [airsonic-ui.api.helpers :as api]))
(re-frame/reg-fx (rf/reg-fx
;; a simple effect to keep println statements out of our event handlers ;; a simple effect to keep println statements out of our event handlers
:log :log
(fn [params] (fn [params]
@ -31,9 +31,9 @@
(assoc effects :dispatch [:credentials/verify credentials]) (assoc effects :dispatch [:credentials/verify credentials])
effects))) effects)))
(re-frame/reg-event-fx (rf/reg-event-fx
::initialize-app ::initialize-app
[(re-frame/inject-cofx :store)] [(rf/inject-cofx :store)]
initialize-app) initialize-app)
(defn verify-credentials (defn verify-credentials
@ -44,7 +44,7 @@
(if (every? string? ((juxt :u :p :server) credentials)) (if (every? string? ((juxt :u :p :server) credentials))
{:dispatch [:credentials/send-authentication-request credentials]})) {:dispatch [:credentials/send-authentication-request credentials]}))
(re-frame/reg-event-fx :credentials/verify verify-credentials) (rf/reg-event-fx :credentials/verify verify-credentials)
;; --- ;; ---
;; auth logic ;; auth logic
@ -57,7 +57,7 @@
{:db (assoc db :credentials credentials) {:db (assoc db :credentials credentials)
:dispatch [:credentials/send-authentication-request credentials]})) :dispatch [:credentials/send-authentication-request credentials]}))
(re-frame/reg-event-fx :credentials/user-login user-login) (rf/reg-event-fx :credentials/user-login user-login)
(defn authentication-request (defn authentication-request
"Tries to authenticate a user by requesting info about the given user, saving "Tries to authenticate a user by requesting info about the given user, saving
@ -69,7 +69,7 @@
:on-success [:credentials/authentication-response credentials] :on-success [:credentials/authentication-response credentials]
:on-failure [:api/failed-response]}}) ; <- we don't need endpoint and params here because the response is not cached :on-failure [:api/failed-response]}}) ; <- we don't need endpoint and params here because the response is not cached
(re-frame/reg-event-fx :credentials/send-authentication-request authentication-request) (rf/reg-event-fx :credentials/send-authentication-request authentication-request)
(defn authentication-response (defn authentication-response
"Since we don't get real status codes, we have to look into the server's "Since we don't get real status codes, we have to look into the server's
@ -79,7 +79,7 @@
[:credentials/authentication-failure response] [:credentials/authentication-failure response]
[:credentials/authentication-success credentials response])}) [:credentials/authentication-success credentials response])})
(re-frame/reg-event-fx :credentials/authentication-response authentication-response) (rf/reg-event-fx :credentials/authentication-response authentication-response)
(defn authentication-failure (defn authentication-failure
"Removes all stored credentials and displays potential api errors to the user" "Removes all stored credentials and displays potential api errors to the user"
@ -88,7 +88,7 @@
:store (dissoc store :credentials) :store (dissoc store :credentials)
:db (dissoc db :credentials)}) :db (dissoc db :credentials)})
(re-frame/reg-event-fx :credentials/authentication-failure authentication-failure) (rf/reg-event-fx :credentials/authentication-failure authentication-failure)
(defn authentication-success (defn authentication-success
"Gets called after the server indicates that the credentials entered by a user "Gets called after the server indicates that the credentials entered by a user
@ -99,7 +99,7 @@
(assoc :user (api/unwrap-response auth-response))) (assoc :user (api/unwrap-response auth-response)))
:dispatch [::logged-in]}) :dispatch [::logged-in]})
(re-frame/reg-event-fx :credentials/authentication-success authentication-success) (rf/reg-event-fx :credentials/authentication-success authentication-success)
(defn logged-in (defn logged-in
[cofx _] [cofx _]
@ -107,9 +107,9 @@
[::routes/library])] [::routes/library])]
{:dispatch [:routes/do-navigation redirect]})) {:dispatch [:routes/do-navigation redirect]}))
(re-frame/reg-event-fx (rf/reg-event-fx
::logged-in ::logged-in
[(re-frame/inject-cofx :routes/from-query-param :redirect)] [(rf/inject-cofx :routes/from-query-param :redirect)]
logged-in) logged-in)
(defn logout (defn logout
@ -123,21 +123,21 @@
:db db/default-db :db db/default-db
:audio/stop nil})) :audio/stop nil}))
(re-frame/reg-event-fx ::logout logout) (rf/reg-event-fx ::logout logout)
;; --- ;; ---
;; routing ;; routing
;; --- ;; ---
(re-frame/reg-event-fx (rf/reg-event-fx
:routes/did-navigate :routes/did-navigate
(fn [{:keys [db]} [_ route params query]] (fn [{:keys [db]} [_ route params query]]
{:db (assoc db :routes/current-route [route params query]) {:db (assoc db :routes/current-route [route params query])
:dispatch-n (routes/route-events route params query)})) :dispatch-n (routes/route-events route params query)}))
(re-frame/reg-event-fx (rf/reg-event-fx
:routes/unauthorized :routes/unauthorized
[(re-frame/inject-cofx :routes/current-route)] [(rf/inject-cofx :routes/current-route)]
(fn [{:routes/keys [current-route]} _] (fn [{:routes/keys [current-route]} _]
{:dispatch [::logout :redirect-to current-route]})) {:dispatch [::logout :redirect-to current-route]}))
@ -161,10 +161,10 @@
:dispatch-later [{:ms (get notification-duration level) :dispatch-later [{:ms (get notification-duration level)
:dispatch [:notification/hide id]}]})) :dispatch [:notification/hide id]}]}))
(re-frame/reg-event-fx :notification/show show-notification) (rf/reg-event-fx :notification/show show-notification)
(defn hide-notification (defn hide-notification
[db [_ notification-id]] [db [_ notification-id]]
(update db :notifications dissoc notification-id)) (update db :notifications dissoc notification-id))
(re-frame/reg-event-db :notification/hide hide-notification) (rf/reg-event-db :notification/hide hide-notification)

View file

@ -1,7 +1,7 @@
(ns airsonic-ui.routes (ns airsonic-ui.routes
(:require [bide.core :as r] (:require [bide.core :as r]
[cljs.reader :refer [read-string]] [cljs.reader :refer [read-string]]
[re-frame.core :as re-frame] [re-frame.core :as rf]
[airsonic-ui.config :as conf])) [airsonic-ui.config :as conf]))
(def default-route ::login) (def default-route ::login)
@ -96,15 +96,15 @@
;; subscription returning the matched route for the current hashbang ;; subscription returning the matched route for the current hashbang
(re-frame/reg-sub :routes/current-route (fn [db _] (:routes/current-route db))) (rf/reg-sub :routes/current-route (fn [db _] (:routes/current-route db)))
;; NOTE: There is some duplication here. The route events are provided as a ;; NOTE: There is some duplication here. The route events are provided as a
;; subscription but they are also invoked directly in events.cljs. It didn't ;; subscription but they are also invoked directly in events.cljs. It didn't
;; seem to justify pulling in a whole library and we need it in our top most view ;; seem to justify pulling in a whole library and we need it in our top most view
(re-frame/reg-sub (rf/reg-sub
:routes/events-for-current-route :routes/events-for-current-route
(fn [db _] (re-frame/subscribe [:routes/current-route])) (fn [db _] (rf/subscribe [:routes/current-route]))
(fn [current-route _] (apply route-events current-route))) (fn [current-route _] (apply route-events current-route)))
;; these are helper effects we can use to navigate; the first two manage an atom ;; these are helper effects we can use to navigate; the first two manage an atom
@ -133,7 +133,7 @@
(apply r/navigate! router route) (apply r/navigate! router route)
(dissoc context :event))))) (dissoc context :event)))))
(re-frame/reg-event-fx :routes/do-navigation do-navigation (fn [& _] nil)) (rf/reg-event-fx :routes/do-navigation do-navigation (fn [& _] nil))
(defn can-access? [route] (defn can-access? [route]
(or (not (protected-routes route)) (or (not (protected-routes route))
@ -143,8 +143,8 @@
[route-id params query] [route-id params query]
#_(println "calling on-navigate with" route credentials') #_(println "calling on-navigate with" route credentials')
(if (can-access? route-id) (if (can-access? route-id)
(re-frame/dispatch [:routes/did-navigate route-id params query]) (rf/dispatch [:routes/did-navigate route-id params query])
(re-frame/dispatch [:routes/unauthorized route-id params query]))) (rf/dispatch [:routes/unauthorized route-id params query])))
(defn encode-route (defn encode-route
"Takes a parsed route and returns a representation that's suitable for "Takes a parsed route and returns a representation that's suitable for
@ -163,13 +163,13 @@
(r/match router (subs (.. js/window -location -hash) 1))) (r/match router (subs (.. js/window -location -hash) 1)))
;; add the current route to our coeffect map ;; add the current route to our coeffect map
(re-frame/reg-cofx (rf/reg-cofx
:routes/current-route :routes/current-route
(fn [coeffects _] (fn [coeffects _]
(assoc coeffects :routes/current-route (current-route)))) (assoc coeffects :routes/current-route (current-route))))
;; add route into from a URL parameter to our coeffect map ;; add route into from a URL parameter to our coeffect map
(re-frame/reg-cofx (rf/reg-cofx
:routes/from-query-param :routes/from-query-param
(fn [coeffects param] (fn [coeffects param]
;; this allows us to encode a complete route in a url fragment; useful for ;; this allows us to encode a complete route in a url fragment; useful for
@ -184,5 +184,5 @@
:on-navigate on-navigate})) :on-navigate on-navigate}))
([_] (start-routing!))) ;; <- 1-arity is for the re-frame effect exposed below ([_] (start-routing!))) ;; <- 1-arity is for the re-frame effect exposed below
(re-frame/reg-fx (rf/reg-fx
:routes/start-routing start-routing!) :routes/start-routing start-routing!)

View file

@ -19,6 +19,7 @@
[airsonic-ui.components.bangpow.views :refer [not-found]] [airsonic-ui.components.bangpow.views :refer [not-found]]
[airsonic-ui.components.collection.views :as collection] [airsonic-ui.components.collection.views :as collection]
[airsonic-ui.components.current-queue.views :refer [current-queue]] [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.library.views :as library]
[airsonic-ui.components.podcast.views :as podcast] [airsonic-ui.components.podcast.views :as podcast]
[airsonic-ui.components.search.views :as search])) [airsonic-ui.components.search.views :as search]))
@ -130,6 +131,7 @@
[route-id :as route] @(subscribe [:routes/current-route])] [route-id :as route] @(subscribe [:routes/current-route])]
[(add-classes :div route-id) [(add-classes :div route-id)
[notification-list notifications] [notification-list notifications]
[keyboard/help-modal]
(if is-booting? (if is-booting?
[:div.app-loading>div.loader] [:div.app-loading>div.loader]
[:div [:div

View file

@ -0,0 +1,20 @@
(ns bulma.modal.events
(:require [re-frame.core :as rf]))
(defn show-modal [db [_ modal-id]]
(assoc-in db [:bulma :visible-modal] modal-id))
(rf/reg-event-db ::show show-modal)
(defn hide-modal [db _]
(update db :bulma dissoc :visible-modal))
(rf/reg-event-db ::hide hide-modal)
(defn toggle-modal [db [_ modal-id]]
(let [visible-modal (get-in db [:bulma :visible-modal])]
(if (= visible-modal modal-id)
(hide-modal db [::hide])
(show-modal db [::show modal-id]))))
(rf/reg-event-db ::toggle toggle-modal)

View file

@ -0,0 +1,19 @@
(ns bulma.modal.subs
(:require [re-frame.core :as rf]))
(defn visible-modal
"Gives us the ID of the currently visible modal"
[db _]
(get-in db [:bulma :visible-modal]))
(rf/reg-sub ::visible-modal visible-modal)
(defn visible?
"Predicate to check the visibility of a single modal"
[visible-modal [_ modal-id]]
(= visible-modal modal-id))
(rf/reg-sub
::visible?
:<- [::visible-modal]
visible?)

View file

@ -0,0 +1,47 @@
(ns bulma.modal.views
(:require [re-frame.core :as rf]
[bulma.modal.events :as ev]
[bulma.modal.subs :as sub]))
(defn hide-modal [_]
(rf/dispatch [::ev/hide]))
(defn modal
"Generic modal; arguments:
options:
{:has-hide-button? boolean
:modal-id :some-identifier}
& children"
[{:keys [has-hide-button? modal-id]} & children]
{:pre [(some? modal-id)]}
(let [visible? @(rf/subscribe [::sub/visible? modal-id])
modal-tag (if visible? :div.modal.is-active :div.modal)]
[modal-tag
[:div.modal-background {:on-click hide-modal}]
(into [:div.modal-content] children)
(when has-hide-button?
[:button.modal-hide.is-large {:aria-label "hide"
:on-click hide-modal}])]))
(defn modal-card
"A card modal that renders content on a background. Arguments:
options:
{:title \"Title of the card\"
:foot [[:div \"An array of hiccup elements\"]]
:modal-id :some-identifier}
& children"
[{:keys [title foot modal-id]} & children]
[modal {:has-hide-button? (not (some? title))
:modal-id modal-id}
(when title
[:div.modal-card-head
[:p.modal-card-title title]
[:button.delete {:aria-label "hide"
:on-click hide-modal}]])
(into [:section.modal-card-body] children)
(when foot
(into [:div.modal-card-foot] foot))])

View file

@ -0,0 +1,36 @@
(ns bulma.modal-test
(:require [cljs.test :refer-macros [deftest testing is]]
[bulma.modal.subs :as sub]
[bulma.modal.events :as ev]))
(enable-console-print!)
(deftest bulma-modals
(testing "Should create a collection of modals if there is none"
(let [new-db (ev/show-modal {} [::ev/show :some-modal-id])]
(is (= :some-modal-id (sub/visible-modal new-db [::sub/visible-modal])))))
(testing "Should hide other modals when displaying a new one"
(let [modal-ids [:some-id-1 :some-id-2 :some-id-3]
new-db (reduce (fn [db modal-id]
(ev/show-modal db [::ev/show modal-id]))
{} modal-ids)]
(is (= :some-id-3 (sub/visible-modal new-db [::sub/visible-modal])))))
(testing "Should remove a modal from the collection when we hide it"
(let [modal-ids [:some-id-1 :some-id-2 :some-id-3]
new-db (-> (reduce (fn [db modal-id]
(ev/show-modal db [::ev/show modal-id]))
{} modal-ids)
(ev/hide-modal [::ev/hide]))]
(is (not (some? (sub/visible-modal new-db [::sub/visible-modal]))))))
(testing "Should tell us about the visibility of a modal with a predicate"
(is (true? (-> (ev/show-modal {} [::ev/show :getting-repetitive])
(sub/visible-modal [::sub/visible-modal])
(sub/visible? [::sub/visible? :getting-repetitive])))))
(testing "Modal toggling"
(is (true? (-> (ev/toggle-modal {} [::ev/toggle :some-generic-modal])
(sub/visible-modal [::sub/visible-modal])
(sub/visible? [::sub/visible? :some-generic-modal]))))
(is (not (true? (-> (ev/toggle-modal {} [::ev/toggle :some-generic-modal])
(ev/toggle-modal [::ev/toggle :some-generic-modal])
(sub/visible-modal [::sub/visible-modal])
(sub/visible? [::sub/visible? :some-generic-modal])))))))