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/audio/core.cljs b/src/cljs/airsonic_ui/audio/core.cljs index 611222d..d0a2f42 100644 --- a/src/cljs/airsonic_ui/audio/core.cljs +++ b/src/cljs/airsonic_ui/audio/core.cljs @@ -2,7 +2,7 @@ "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 our re-frame app." - (:require [re-frame.core :as re-frame] + (:require [re-frame.core :as rf] [airsonic-ui.audio.playlist :as playlist] [goog.functions :refer [throttle]])) @@ -29,13 +29,13 @@ (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"]] (.addEventListener el event emit-audio-update)))) ;; effects to be fired from event handlers -(re-frame/reg-fx +(rf/reg-fx :audio/play (fn [stream-url] (when-not @audio @@ -45,19 +45,19 @@ (set! (.-src @audio) stream-url) (.play @audio))) -(re-frame/reg-fx +(rf/reg-fx :audio/pause (fn [_] (some-> @audio .pause))) -(re-frame/reg-fx +(rf/reg-fx :audio/stop (fn [_] (when-let [audio @audio] (.pause audio) (set! (.-currentTime audio) 0)))) -(re-frame/reg-fx +(rf/reg-fx :audio/toggle-play-pause (fn [_] (if-let [a @audio] @@ -65,7 +65,7 @@ (.play a) (.pause a))))) -(re-frame/reg-fx +(rf/reg-fx :audio/seek (fn [[percentage duration]] (set! (. @audio -currentTime) @@ -78,16 +78,16 @@ [db _] (:audio db)) -(re-frame/reg-sub :audio/summary summary) +(rf/reg-sub :audio/summary summary) (defn playlist "Lists the complete playlist" [summary _] (:playlist summary)) -(re-frame/reg-sub +(rf/reg-sub :audio/playlist - (fn [_ _] (re-frame/subscribe [:audio/summary])) + (fn [_ _] (rf/subscribe [:audio/summary])) playlist) (defn current-song @@ -96,9 +96,9 @@ [playlist _] (playlist/peek playlist)) -(re-frame/reg-sub +(rf/reg-sub :audio/current-song - (fn [_ _] (re-frame/subscribe [:audio/playlist])) + (fn [_ _] (rf/subscribe [:audio/playlist])) current-song) (defn playback-status @@ -106,9 +106,9 @@ [summary _] (:playback-status summary)) -(re-frame/reg-sub +(rf/reg-sub :audio/playback-status - (fn [_ _] (re-frame/subscribe [:audio/summary])) + (fn [_ _] (rf/subscribe [:audio/summary])) playback-status) (defn is-playing? @@ -117,7 +117,7 @@ (and (not (:paused? playback-status)) (not (:ended? playback-status)))) -(re-frame/reg-sub +(rf/reg-sub :audio/is-playing? - (fn [_ _] (re-frame/subscribe [:audio/playback-status])) + (fn [_ _] (rf/subscribe [:audio/playback-status])) is-playing?) diff --git a/src/cljs/airsonic_ui/components/audio_player/events.cljs b/src/cljs/airsonic_ui/components/audio_player/events.cljs index 1a5b7bb..615dcbb 100644 --- a/src/cljs/airsonic_ui/components/audio_player/events.cljs +++ b/src/cljs/airsonic_ui/components/audio_player/events.cljs @@ -1,9 +1,9 @@ (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.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 :audio-player/play-all (fn [{:keys [db]} [_ songs start-idx]] @@ -12,17 +12,17 @@ {:audio/play (api/stream-url (:credentials db) (playlist/peek playlist)) :db (assoc-in db [:audio :playlist] playlist)}))) -(re-frame/reg-event-db +(rf/reg-event-db :audio-player/set-playback-mode (fn [db [_ 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 (fn [db [_ 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 (fn [{:keys [db]} _] (let [db (update-in db [:audio :playlist] playlist/next-song) @@ -30,7 +30,7 @@ {:db db :audio/play (api/stream-url (:credentials db) next)}))) -(re-frame/reg-event-fx +(rf/reg-event-fx :audio-player/previous-song (fn [{:keys [db]} _] (let [db (update-in db [:audio :playlist] playlist/previous-song) @@ -38,17 +38,17 @@ {:db db :audio/play (api/stream-url (:credentials db) prev)}))) -(re-frame/reg-event-db +(rf/reg-event-db :audio-player/enqueue-next (fn [db [_ song]] (update-in db [:audio :playlist] #(playlist/enqueue-next % song)))) -(re-frame/reg-event-db +(rf/reg-event-db :audio-player/enqueue-last (fn [db [_ song]] (update-in db [:audio :playlist] #(playlist/enqueue-last % song)))) -(re-frame/reg-event-fx +(rf/reg-event-fx :audio-player/toggle-play-pause (fn [_ _] {:audio/toggle-play-pause nil})) @@ -60,9 +60,9 @@ (cond-> {:db (assoc-in db [:audio :playback-status] status)} (: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 (fn [{:keys [db]} [_ percentage]] (let [duration (:duration (playlist/peek (get-in db [:audio :playlist])))] 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/components/library/subs.cljs b/src/cljs/airsonic_ui/components/library/subs.cljs index 92211d8..1e79854 100644 --- a/src/cljs/airsonic_ui/components/library/subs.cljs +++ b/src/cljs/airsonic_ui/components/library/subs.cljs @@ -1,5 +1,5 @@ (ns airsonic-ui.components.library.subs - (:require [re-frame.core :as re-frame] + (:require [re-frame.core :as rf] [airsonic-ui.config :as conf])) ;; first some helper functions to make the structure a bit clearer @@ -34,7 +34,7 @@ (map (fn [[k v]] [(inc k) v])) (into (sorted-map)))) -(re-frame/reg-sub +(rf/reg-sub :library/paginated :<- [:api/responses-for-endpoint "getAlbumList2"] paginated-library) diff --git a/src/cljs/airsonic_ui/core.cljs b/src/cljs/airsonic_ui/core.cljs index 93e6579..9ee6cd8 100644 --- a/src/cljs/airsonic_ui/core.cljs +++ b/src/cljs/airsonic_ui/core.cljs @@ -1,6 +1,6 @@ (ns airsonic-ui.core (:require [reagent.core :as reagent] - [re-frame.core :as re-frame] + [re-frame.core :as rf] ;; 3rd party effects / coeffects [day8.re-frame.http-fx] [akiroz.re-frame.storage :as storage] @@ -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] @@ -24,12 +25,12 @@ (println "dev mode"))) (defn mount-root [] - (re-frame/clear-subscription-cache!) + (rf/clear-subscription-cache!) (reagent/render [views/main-panel] (.getElementById js/document "app"))) (defn ^:export init [] - (storage/reg-co-fx! :airsonic-ui {:fx :store - :cofx :store}) - (re-frame/dispatch-sync [::events/initialize-app]) + (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/events.cljs b/src/cljs/airsonic_ui/events.cljs index 7bd9e42..85308eb 100644 --- a/src/cljs/airsonic_ui/events.cljs +++ b/src/cljs/airsonic_ui/events.cljs @@ -1,11 +1,11 @@ (ns airsonic-ui.events - (:require [re-frame.core :as re-frame] + (:require [re-frame.core :as rf] [ajax.core :as ajax] [airsonic-ui.routes :as routes] [airsonic-ui.db :as db] [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 :log (fn [params] @@ -31,9 +31,9 @@ (assoc effects :dispatch [:credentials/verify credentials]) effects))) -(re-frame/reg-event-fx +(rf/reg-event-fx ::initialize-app - [(re-frame/inject-cofx :store)] + [(rf/inject-cofx :store)] initialize-app) (defn verify-credentials @@ -44,7 +44,7 @@ (if (every? string? ((juxt :u :p :server) 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 @@ -57,7 +57,7 @@ {:db (assoc db :credentials 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 "Tries to authenticate a user by requesting info about the given user, saving @@ -69,7 +69,7 @@ :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 -(re-frame/reg-event-fx :credentials/send-authentication-request authentication-request) +(rf/reg-event-fx :credentials/send-authentication-request authentication-request) (defn authentication-response "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-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 "Removes all stored credentials and displays potential api errors to the user" @@ -88,7 +88,7 @@ :store (dissoc store :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 "Gets called after the server indicates that the credentials entered by a user @@ -99,7 +99,7 @@ (assoc :user (api/unwrap-response auth-response))) :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 [cofx _] @@ -107,9 +107,9 @@ [::routes/library])] {:dispatch [:routes/do-navigation redirect]})) -(re-frame/reg-event-fx +(rf/reg-event-fx ::logged-in - [(re-frame/inject-cofx :routes/from-query-param :redirect)] + [(rf/inject-cofx :routes/from-query-param :redirect)] logged-in) (defn logout @@ -123,21 +123,21 @@ :db db/default-db :audio/stop nil})) -(re-frame/reg-event-fx ::logout logout) +(rf/reg-event-fx ::logout logout) ;; --- ;; routing ;; --- -(re-frame/reg-event-fx +(rf/reg-event-fx :routes/did-navigate (fn [{:keys [db]} [_ route params query]] {:db (assoc db :routes/current-route [route params query]) :dispatch-n (routes/route-events route params query)})) -(re-frame/reg-event-fx +(rf/reg-event-fx :routes/unauthorized - [(re-frame/inject-cofx :routes/current-route)] + [(rf/inject-cofx :routes/current-route)] (fn [{:routes/keys [current-route]} _] {:dispatch [::logout :redirect-to current-route]})) @@ -161,10 +161,10 @@ :dispatch-later [{:ms (get notification-duration level) :dispatch [:notification/hide id]}]})) -(re-frame/reg-event-fx :notification/show show-notification) +(rf/reg-event-fx :notification/show show-notification) (defn hide-notification [db [_ 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) diff --git a/src/cljs/airsonic_ui/routes.cljs b/src/cljs/airsonic_ui/routes.cljs index 78f030d..21b7722 100644 --- a/src/cljs/airsonic_ui/routes.cljs +++ b/src/cljs/airsonic_ui/routes.cljs @@ -1,7 +1,7 @@ (ns airsonic-ui.routes (:require [bide.core :as r] [cljs.reader :refer [read-string]] - [re-frame.core :as re-frame] + [re-frame.core :as rf] [airsonic-ui.config :as conf])) (def default-route ::login) @@ -96,15 +96,15 @@ ;; 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 ;; 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 -(re-frame/reg-sub +(rf/reg-sub :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))) ;; these are helper effects we can use to navigate; the first two manage an atom @@ -133,7 +133,7 @@ (apply r/navigate! router route) (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] (or (not (protected-routes route)) @@ -143,8 +143,8 @@ [route-id params query] #_(println "calling on-navigate with" route credentials') (if (can-access? route-id) - (re-frame/dispatch [:routes/did-navigate route-id params query]) - (re-frame/dispatch [:routes/unauthorized route-id params query]))) + (rf/dispatch [:routes/did-navigate route-id params query]) + (rf/dispatch [:routes/unauthorized route-id params query]))) (defn encode-route "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))) ;; add the current route to our coeffect map -(re-frame/reg-cofx +(rf/reg-cofx :routes/current-route (fn [coeffects _] (assoc coeffects :routes/current-route (current-route)))) ;; add route into from a URL parameter to our coeffect map -(re-frame/reg-cofx +(rf/reg-cofx :routes/from-query-param (fn [coeffects param] ;; this allows us to encode a complete route in a url fragment; useful for @@ -184,5 +184,5 @@ :on-navigate on-navigate})) ([_] (start-routing!))) ;; <- 1-arity is for the re-frame effect exposed below -(re-frame/reg-fx +(rf/reg-fx :routes/start-routing start-routing!) 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 diff --git a/src/cljs/bulma/modal/events.cljs b/src/cljs/bulma/modal/events.cljs new file mode 100644 index 0000000..15cc644 --- /dev/null +++ b/src/cljs/bulma/modal/events.cljs @@ -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) diff --git a/src/cljs/bulma/modal/subs.cljs b/src/cljs/bulma/modal/subs.cljs new file mode 100644 index 0000000..291f016 --- /dev/null +++ b/src/cljs/bulma/modal/subs.cljs @@ -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?) diff --git a/src/cljs/bulma/modal/views.cljs b/src/cljs/bulma/modal/views.cljs new file mode 100644 index 0000000..107048c --- /dev/null +++ b/src/cljs/bulma/modal/views.cljs @@ -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))]) diff --git a/test/cljs/bulma/modal_test.cljs b/test/cljs/bulma/modal_test.cljs new file mode 100644 index 0000000..6079a99 --- /dev/null +++ b/test/cljs/bulma/modal_test.cljs @@ -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])))))))