mirror of
https://github.com/heyarne/airsonic-ui.git
synced 2026-05-06 18:33:38 +02:00
Merge branch 'enhancement/improve-audio-player'
This commit is contained in:
commit
2638378064
10 changed files with 187 additions and 69 deletions
|
|
@ -20,8 +20,10 @@
|
||||||
(doseq [event ["loadstart" "progress" "play" "timeupdate" "pause"]]
|
(doseq [event ["loadstart" "progress" "play" "timeupdate" "pause"]]
|
||||||
(.addEventListener el event #(re-frame/dispatch [:audio/update (->status el)]))))
|
(.addEventListener el event #(re-frame/dispatch [:audio/update (->status el)]))))
|
||||||
|
|
||||||
|
;; effects to be fired from event handlers
|
||||||
|
|
||||||
(re-frame/reg-fx
|
(re-frame/reg-fx
|
||||||
:play-song
|
:audio/play
|
||||||
(fn [song-url]
|
(fn [song-url]
|
||||||
(when-not @audio
|
(when-not @audio
|
||||||
(reset! audio (js/Audio.))
|
(reset! audio (js/Audio.))
|
||||||
|
|
@ -31,9 +33,74 @@
|
||||||
(.play @audio)))
|
(.play @audio)))
|
||||||
|
|
||||||
(re-frame/reg-fx
|
(re-frame/reg-fx
|
||||||
:toggle-play-pause
|
:audio/pause
|
||||||
(fn [_]
|
(fn [_]
|
||||||
(let [a @audio]
|
(some-> @audio .pause)))
|
||||||
|
|
||||||
|
(re-frame/reg-fx
|
||||||
|
:audio/stop
|
||||||
|
(fn [_]
|
||||||
|
(when-let [audio @audio]
|
||||||
|
(.pause audio)
|
||||||
|
(set! (.-currentTime audio) 0))))
|
||||||
|
|
||||||
|
(re-frame/reg-fx
|
||||||
|
:audio/toggle-play-pause
|
||||||
|
(fn [_]
|
||||||
|
(if-let [a @audio]
|
||||||
(if (.-paused a)
|
(if (.-paused a)
|
||||||
(.play a)
|
(.play a)
|
||||||
(.pause a)))))
|
(.pause a)))))
|
||||||
|
|
||||||
|
;; subscriptions
|
||||||
|
|
||||||
|
(defn summary
|
||||||
|
"Returns all information about audio that we have"
|
||||||
|
[db _]
|
||||||
|
(:audio db))
|
||||||
|
|
||||||
|
(re-frame/reg-sub :audio/summary summary)
|
||||||
|
|
||||||
|
(defn current-song
|
||||||
|
"Gives us information about the currently played song as presented by
|
||||||
|
the airsonic api"
|
||||||
|
[summary _]
|
||||||
|
(:current-song summary))
|
||||||
|
|
||||||
|
(re-frame/reg-sub
|
||||||
|
:audio/current-song
|
||||||
|
(fn [_ _] (re-frame/subscribe [:audio/summary]))
|
||||||
|
current-song)
|
||||||
|
|
||||||
|
(defn playback-status
|
||||||
|
"Gives us information about the most recently fired html 5 audio event"
|
||||||
|
[summary _]
|
||||||
|
(:playback-status summary))
|
||||||
|
|
||||||
|
(re-frame/reg-sub
|
||||||
|
:audio/playback-status
|
||||||
|
(fn [_ _] (re-frame/subscribe [:audio/summary]))
|
||||||
|
playback-status)
|
||||||
|
|
||||||
|
(defn is-playing?
|
||||||
|
"Predicate to tell us whether we currently have audio output or not"
|
||||||
|
[playback-status _]
|
||||||
|
(and (not (:paused? playback-status))
|
||||||
|
(not (:ended? playback-status))))
|
||||||
|
|
||||||
|
(re-frame/reg-sub
|
||||||
|
:audio/is-playing?
|
||||||
|
(fn [_ _] (re-frame/subscribe [:audio/current-playback-status]))
|
||||||
|
is-playing?)
|
||||||
|
|
||||||
|
(comment
|
||||||
|
;; NOTE: Not in use currently
|
||||||
|
(defn current-playlist
|
||||||
|
"Lists the complete playlist"
|
||||||
|
[summary _]
|
||||||
|
(:playlist summary))
|
||||||
|
|
||||||
|
(re-frame/reg-sub
|
||||||
|
:audio/current-playlist
|
||||||
|
(fn [_ _] (re-frame/subscribe [:audio/summary]))
|
||||||
|
current-playlist))
|
||||||
|
|
|
||||||
|
|
@ -130,8 +130,8 @@
|
||||||
[::routes/login {} {:redirect (routes/encode-route redirect)}]
|
[::routes/login {} {:redirect (routes/encode-route redirect)}]
|
||||||
[::routes/login])]
|
[::routes/login])]
|
||||||
:store nil
|
:store nil
|
||||||
:db (-> (merge (:db cofx) db/default-db)
|
:db db/default-db
|
||||||
(dissoc :credentials))}))
|
:audio/stop nil}))
|
||||||
|
|
||||||
(re-frame/reg-event-fx ::logout logout)
|
(re-frame/reg-event-fx ::logout logout)
|
||||||
|
|
||||||
|
|
@ -180,41 +180,40 @@
|
||||||
; 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
|
||||||
::play-songs
|
::play-songs
|
||||||
(fn [{:keys [db]} [_ songs song]]
|
(fn [{:keys [db]} [_ songs song]]
|
||||||
{:play-song (song-url db song)
|
{:audio/play (song-url db song)
|
||||||
:db (-> db
|
:db (-> (assoc-in db [:audio :current-song] song)
|
||||||
(assoc-in [:currently-playing :item] song)
|
(assoc-in [:audio :playlist] songs))}))
|
||||||
(assoc-in [:currently-playing :playlist] songs))}))
|
|
||||||
|
|
||||||
(re-frame/reg-event-fx
|
(re-frame/reg-event-fx
|
||||||
::next-song
|
::next-song
|
||||||
(fn [{:keys [db]} _]
|
(fn [{:keys [db]} _]
|
||||||
(let [playlist (-> db :currently-playing :playlist)
|
(let [playlist (get-in db [:audio :playlist])
|
||||||
current (-> db :currently-playing :item)
|
current-song (get-in db [:audio :current-song])
|
||||||
next (first (rest (drop-while #(not= % current) playlist)))]
|
next (first (rest (drop-while #(not= % current-song) playlist)))]
|
||||||
(when next
|
(when next
|
||||||
{:play-song (song-url db next)
|
{:audio/play (song-url db next)
|
||||||
:db (assoc-in db [:currently-playing :item] next)}))))
|
:db (assoc-in db [:audio :current-song] next)}))))
|
||||||
|
|
||||||
(re-frame/reg-event-fx
|
(re-frame/reg-event-fx
|
||||||
::previous-song
|
::previous-song
|
||||||
(fn [{:keys [db]} _]
|
(fn [{:keys [db]} _]
|
||||||
(let [playlist (-> db :currently-playing :playlist)
|
(let [playlist (get-in db [:audio :playlist])
|
||||||
current (-> db :currently-playing :item)
|
current-song (get-in db [:audio :current-song])
|
||||||
previous (last (take-while #(not= % current) playlist))]
|
previous (last (take-while #(not= % current-song) playlist))]
|
||||||
(when previous
|
(when previous
|
||||||
{:play-song (song-url db previous)
|
{:audio/play (song-url db previous)
|
||||||
:db (assoc-in db [:currently-playing :item] previous)}))))
|
:db (assoc-in db [:audio :current-song] previous)}))))
|
||||||
|
|
||||||
(re-frame/reg-event-fx
|
(re-frame/reg-event-fx
|
||||||
::toggle-play-pause
|
::toggle-play-pause
|
||||||
(fn [_ _]
|
(fn [_ _]
|
||||||
{:toggle-play-pause nil}))
|
{:audio/toggle-play-pause nil}))
|
||||||
|
|
||||||
(re-frame/reg-event-db
|
(re-frame/reg-event-db
|
||||||
:audio/update
|
:audio/update
|
||||||
(fn [db [_ status]]
|
(fn [db [_ status]]
|
||||||
; we receive this from the player once it's playing
|
; this is coming from HTML5 Audio events
|
||||||
(assoc-in db [:currently-playing :status] status)))
|
(assoc-in db [:audio :playback-status] status)))
|
||||||
|
|
||||||
;; ---
|
;; ---
|
||||||
;; routing
|
;; routing
|
||||||
|
|
|
||||||
|
|
@ -47,21 +47,6 @@
|
||||||
(fn [db _]
|
(fn [db _]
|
||||||
(:response db)))
|
(:response db)))
|
||||||
|
|
||||||
(re-frame/reg-sub
|
|
||||||
; returns info on the current song as is (basically the metadata you can read from the file system)
|
|
||||||
::currently-playing
|
|
||||||
(fn [db _]
|
|
||||||
(:currently-playing db)))
|
|
||||||
|
|
||||||
(re-frame/reg-sub
|
|
||||||
::is-playing?
|
|
||||||
(fn [query-v _]
|
|
||||||
[(re-frame/subscribe [::currently-playing])])
|
|
||||||
(fn [[currently-playing] _]
|
|
||||||
(let [status (:status currently-playing)]
|
|
||||||
(and (not (:paused? status))
|
|
||||||
(not (:ended? status))))))
|
|
||||||
|
|
||||||
;; user notifications
|
;; user notifications
|
||||||
|
|
||||||
(defn notifications [db _] (:notifications db))
|
(defn notifications [db _] (:notifications db))
|
||||||
|
|
|
||||||
|
|
@ -67,9 +67,6 @@
|
||||||
(let [notifications @(subscribe [::subs/notifications])
|
(let [notifications @(subscribe [::subs/notifications])
|
||||||
is-booting? @(subscribe [::subs/is-booting?])
|
is-booting? @(subscribe [::subs/is-booting?])
|
||||||
[route-id params query] @(subscribe [::subs/current-route])]
|
[route-id params query] @(subscribe [::subs/current-route])]
|
||||||
(println "route-id" route-id (case route-id
|
|
||||||
::routes/login "::routes/login"
|
|
||||||
"something else"))
|
|
||||||
[:div
|
[:div
|
||||||
[notification-list notifications]
|
[notification-list notifications]
|
||||||
(if is-booting?
|
(if is-booting?
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
(ns airsonic-ui.views.bottom-bar
|
(ns airsonic-ui.views.bottom-bar
|
||||||
(:require [re-frame.core :refer [dispatch subscribe]]
|
(:require [re-frame.core :refer [dispatch subscribe]]
|
||||||
[airsonic-ui.events :as events]
|
[airsonic-ui.events :as events]
|
||||||
[airsonic-ui.subs :as subs]
|
|
||||||
[airsonic-ui.views.cover :refer [cover]]
|
[airsonic-ui.views.cover :refer [cover]]
|
||||||
[airsonic-ui.views.icon :refer [icon]]))
|
[airsonic-ui.views.icon :refer [icon]]))
|
||||||
|
|
||||||
;; currently playing / coming next / audio controls...
|
;; currently playing / coming next / audio controls...
|
||||||
|
|
||||||
(defn current-song-info [{:keys [item status]}]
|
(defn current-song-info [song status]
|
||||||
[:article
|
[:article
|
||||||
[:div (:artist item) " - " (:title item)]
|
[:div (:artist song) " - " (:title song)]
|
||||||
;; FIXME: Sometimes items don't have a duration
|
;; FIXME: Sometimes items don't have a duration
|
||||||
[:progress.progress.is-tiny {:value (:current-time status)
|
[:progress.progress.is-tiny {:value (:current-time status)
|
||||||
:max (:duration item)}]])
|
:max (:duration song)}]])
|
||||||
|
|
||||||
(defn playback-controls [is-playing?]
|
(defn playback-controls [is-playing?]
|
||||||
[:div.field.has-addons
|
[:div.field.has-addons
|
||||||
|
|
@ -28,19 +27,20 @@
|
||||||
(def logo-url "https://airsonic.github.io/airsonic-ui/assets/images/logo/airsonic-light-350x100.png")
|
(def logo-url "https://airsonic.github.io/airsonic-ui/assets/images/logo/airsonic-light-350x100.png")
|
||||||
|
|
||||||
(defn bottom-bar []
|
(defn bottom-bar []
|
||||||
(let [currently-playing @(subscribe [::subs/currently-playing])
|
(let [current-song @(subscribe [:audio/current-song])
|
||||||
is-playing? @(subscribe [::subs/is-playing?])]
|
playback-status @(subscribe [:audio/playback-status])
|
||||||
|
is-playing? @(subscribe [:audio/is-playing?])]
|
||||||
[:nav.navbar.is-fixed-bottom.playback-area
|
[:nav.navbar.is-fixed-bottom.playback-area
|
||||||
[:div.navbar-brand
|
[:div.navbar-brand
|
||||||
[:div.navbar-item
|
[:div.navbar-item
|
||||||
[:img {:src logo-url}]]]
|
[:img {:src logo-url}]]]
|
||||||
[:div.navbar-menu.is-active
|
[:div.navbar-menu.is-active
|
||||||
(if currently-playing
|
(if current-song
|
||||||
;; show song info
|
;; show song info
|
||||||
[:section.level.audio-interaction
|
[:section.level.audio-interaction
|
||||||
[:div.level-left>article.media
|
[:div.level-left>article.media
|
||||||
[:div.media-left [cover (:item currently-playing) 48]]
|
[:div.media-left [cover current-song 48]]
|
||||||
[:div.media-content [current-song-info currently-playing]]]
|
[:div.media-content [current-song-info current-song playback-status]]]
|
||||||
[:div.level-right [playback-controls is-playing?]]]
|
[:div.level-right [playback-controls is-playing?]]]
|
||||||
;; not playing anything
|
;; not playing anything
|
||||||
[:p.idle-notification "Currently no song selected"])]]))
|
[:p.idle-notification "Currently no song selected"])]]))
|
||||||
|
|
|
||||||
40
test/cljs/airsonic_ui/audio_test.cljs
Normal file
40
test/cljs/airsonic_ui/audio_test.cljs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
(ns airsonic-ui.audio-test
|
||||||
|
(:require [airsonic-ui.audio :as audio]
|
||||||
|
[airsonic-ui.fixtures :as fixtures]
|
||||||
|
[airsonic-ui.test-helpers :as helpers]
|
||||||
|
[cljs.test :refer [deftest testing is]]))
|
||||||
|
|
||||||
|
(enable-console-print!)
|
||||||
|
|
||||||
|
(defn- simulate-playlist [n ]
|
||||||
|
(repeatedly n #(hash-map :id (rand-int 9999)
|
||||||
|
:coverArt (rand-int 9999)
|
||||||
|
:year (+ 1900 (rand-int 118))
|
||||||
|
:artist (helpers/rand-str)
|
||||||
|
:aristId (rand-int 100000)
|
||||||
|
:title (helpers/rand-str)
|
||||||
|
:album (helpers/rand-str))))
|
||||||
|
|
||||||
|
(def fixture
|
||||||
|
{:audio {:current-song fixtures/song
|
||||||
|
:playlist (simulate-playlist 20)
|
||||||
|
:playback-status fixtures/playback-status}})
|
||||||
|
|
||||||
|
(deftest current-song
|
||||||
|
(letfn [(current-song [db]
|
||||||
|
(-> (audio/summary db [:audio/summary])
|
||||||
|
(audio/current-song [:audio/current-song])))]
|
||||||
|
(testing "Should provide information about the song"
|
||||||
|
(= fixtures/song (current-song fixture)))))
|
||||||
|
|
||||||
|
(deftest playback-status
|
||||||
|
(letfn [(is-playing? [playback-status]
|
||||||
|
(audio/is-playing? playback-status [:audio/is-playing?]))]
|
||||||
|
(testing "Should be shown as not playing when the song is paused or has ended"
|
||||||
|
(is (not (is-playing? {:paused? true, :ended? false})))
|
||||||
|
(is (not (is-playing? {:paused? false, :ended? true}))))
|
||||||
|
(testing "Should be shown as playing when the song is not paused or finished"
|
||||||
|
(is (is-playing? {:paused? false, :ended? false})))))
|
||||||
|
|
||||||
|
#_(deftest current-playlist
|
||||||
|
(testing "Should show the complete playlist"))
|
||||||
|
|
@ -93,7 +93,9 @@
|
||||||
(testing "Should redirect to the login screen"
|
(testing "Should redirect to the login screen"
|
||||||
(is (dispatches? fx [:routes/do-navigation [::routes/login]])))
|
(is (dispatches? fx [:routes/do-navigation [::routes/login]])))
|
||||||
(testing "Should reset the app-db"
|
(testing "Should reset the app-db"
|
||||||
(is (= db/default-db (:db fx)))))
|
(is (= db/default-db (:db fx))))
|
||||||
|
(testing "Should stop currently playing songs"
|
||||||
|
(is (contains? fx :audio/stop))))
|
||||||
(testing "Should be able to keep a redirection parameter"
|
(testing "Should be able to keep a redirection parameter"
|
||||||
(let [redirect [:route {:with-data #{1 2 3 4 5}}]
|
(let [redirect [:route {:with-data #{1 2 3 4 5}}]
|
||||||
navigation-event (:dispatch (events/logout {} [:_ :redirect-to redirect]))]
|
navigation-event (:dispatch (events/logout {} [:_ :redirect-to redirect]))]
|
||||||
|
|
@ -102,13 +104,15 @@
|
||||||
(is (= ::routes/login route-id))
|
(is (= ::routes/login route-id))
|
||||||
(is (contains? query :redirect))))))
|
(is (contains? query :redirect))))))
|
||||||
|
|
||||||
(defn- first-notification [fx]
|
|
||||||
(-> (get-in fx [:db :notifications]) vals first))
|
|
||||||
|
|
||||||
(deftest api-interaction
|
(deftest api-interaction
|
||||||
(testing "Should show an error notification when airsonic responds with an error"
|
(testing "Should show an error notification when airsonic responds with an error"
|
||||||
(let [fx (events/good-api-response {} [:_ (:error fixtures/responses)])]
|
(let [fx (events/good-api-response {} [:_ (:error fixtures/responses)])
|
||||||
(is (= :error (-> fx :dispatch second))))))
|
ev (:dispatch fx)]
|
||||||
|
(is (= :notification/show (first ev)))
|
||||||
|
(is (= :error (second ev))))))
|
||||||
|
|
||||||
|
(defn- first-notification [fx]
|
||||||
|
(-> (get-in fx [:db :notifications]) vals first))
|
||||||
|
|
||||||
(deftest user-notifications
|
(deftest user-notifications
|
||||||
(testing "Should be able to display a message with an assigned level"
|
(testing "Should be able to display a message with an assigned level"
|
||||||
|
|
|
||||||
|
|
@ -43,3 +43,11 @@
|
||||||
:contentType "audio/mpeg",
|
:contentType "audio/mpeg",
|
||||||
:album "Reincarnations, Pt. 2 - The Remix Chapter 2009 - 2014",
|
:album "Reincarnations, Pt. 2 - The Remix Chapter 2009 - 2014",
|
||||||
:track 14})
|
:track 14})
|
||||||
|
|
||||||
|
(def playback-status
|
||||||
|
{:ended? false
|
||||||
|
:loop? false
|
||||||
|
:muted? false
|
||||||
|
:paused? false
|
||||||
|
:current-src "https://londe.arnes.space/rest/stream?f=json&c=airsonic-ui-cljs&v=1.15.0&id=9574&u=arne&p=27h-%25bO%5B8-.ys%40SQ%7Bg%24-%5B5NZkX%7Dw%24NNwY%263DPATi%2CgaFoH%40e"
|
||||||
|
:current-time 3.477029})
|
||||||
|
|
|
||||||
|
|
@ -6,3 +6,14 @@
|
||||||
[cofx ev]
|
[cofx ev]
|
||||||
(let [all-events (conj (get cofx :dispatch-n []) (:dispatch cofx))]
|
(let [all-events (conj (get cofx :dispatch-n []) (:dispatch cofx))]
|
||||||
(boolean (some #(= ev (if (vector? ev) % (first %))) all-events))))
|
(boolean (some #(= ev (if (vector? ev) % (first %))) all-events))))
|
||||||
|
|
||||||
|
(defn rand-str
|
||||||
|
"Generates a random string; ported from https://stackoverflow.com/a/27747377/2345852"
|
||||||
|
([] (rand-str 40))
|
||||||
|
([len]
|
||||||
|
(let [arr (js/Uint8Array. (/ len 2))]
|
||||||
|
(.. js/window -crypto (getRandomValues arr))
|
||||||
|
(.. js/Array
|
||||||
|
(from arr #(-> (str 0 (.toString % 16))
|
||||||
|
(.substr -2)))
|
||||||
|
(join "")))))
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,24 @@
|
||||||
(ns airsonic-ui.test-helpers-test
|
(ns airsonic-ui.test-helpers-test
|
||||||
(:require [cljs.test :refer [deftest testing is]]
|
(:require [cljs.test :refer [deftest testing is]]
|
||||||
[airsonic-ui.test-helpers :refer [dispatches?]]))
|
[airsonic-ui.test-helpers :as h]))
|
||||||
|
|
||||||
(deftest dispatch-helper
|
(deftest dispatch-helper
|
||||||
(testing "single dispatch"
|
(testing "Should identify singly dispatched events"
|
||||||
(is (false? (dispatches? {} :foo)))
|
(is (false? (h/dispatches? {} :foo)))
|
||||||
(is (true? (dispatches? {:dispatch [:foo 1 2 3]} :foo)))
|
(is (true? (h/dispatches? {:dispatch [:foo 1 2 3]} :foo)))
|
||||||
(is (false? (dispatches? {:dispatch [:foo 1 2 3]} :bar)))
|
(is (false? (h/dispatches? {:dispatch [:foo 1 2 3]} :bar)))
|
||||||
(is (true? (dispatches? {:dispatch [:foo 1 2 3]} [:foo 1 2 3])))
|
(is (true? (h/dispatches? {:dispatch [:foo 1 2 3]} [:foo 1 2 3])))
|
||||||
(is (false? (dispatches? {:dispatch [:foo 1 2 3]} [:bar 2 3]))))
|
(is (false? (h/dispatches? {:dispatch [:foo 1 2 3]} [:bar 2 3]))))
|
||||||
(testing "multiple dispatch"
|
(testing "Should identify an event along multiple dispatched events"
|
||||||
(is (false? (dispatches? {:dispatch-n [[:bar]]} :foo)))
|
(is (false? (h/dispatches? {:dispatch-n [[:bar]]} :foo)))
|
||||||
(is (true? (dispatches? {:dispatch-n [[:foo 1 2 3]]} :foo)))
|
(is (true? (h/dispatches? {:dispatch-n [[:foo 1 2 3]]} :foo)))
|
||||||
(is (false? (dispatches? {:dispatch-n [[:foo 1 2 3]]} :bar)))
|
(is (false? (h/dispatches? {:dispatch-n [[:foo 1 2 3]]} :bar)))
|
||||||
(is (dispatches? {:dispatch-n [[:foo 1 2 3]]} [:foo 1 2 3]))
|
(is (h/dispatches? {:dispatch-n [[:foo 1 2 3]]} [:foo 1 2 3]))
|
||||||
(is (false? (dispatches? {:dispatch-n [[:foo 1 2 3]]} [:bar 2 3])))))
|
(is (false? (h/dispatches? {:dispatch-n [[:foo 1 2 3]]} [:bar 2 3])))))
|
||||||
|
|
||||||
|
(deftest rand-str
|
||||||
|
(testing "Generates strings"
|
||||||
|
(is (string? (h/rand-str)))
|
||||||
|
(is (string? (h/rand-str 20))))
|
||||||
|
(testing "Should respect the length for even lengths"
|
||||||
|
(is (= 124 (count (h/rand-str 124))))))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue