1
0
Fork 0
mirror of https://github.com/heyarne/airsonic-ui.git synced 2026-05-06 10:23:39 +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
},
"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"

View file

@ -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"
}
}

View file

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

View file

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

View file

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

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
(: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)

View file

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

View file

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

View file

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

View file

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

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