diff --git a/src/cljs/airsonic_ui/events.cljs b/src/cljs/airsonic_ui/events.cljs index f58d053..67599a6 100644 --- a/src/cljs/airsonic_ui/events.cljs +++ b/src/cljs/airsonic_ui/events.cljs @@ -180,7 +180,6 @@ ; sets up the db, starts to play a song and adds the rest to a playlist ::play-songs (fn [{:keys [db]} [_ songs start-idx]] - (println "play-songs called with" start-idx songs) (let [playlist (-> (playlist/->playlist songs :playback-mode :linear :repeat-mode :repeat-all) (playlist/set-current-song start-idx))] {:audio/play (song-url db (playlist/peek playlist)) @@ -229,11 +228,14 @@ (fn [_ _] {:audio/toggle-play-pause nil})) -(re-frame/reg-event-db - :audio/update - (fn [db [_ status]] - ; this is coming from HTML5 Audio events - (assoc-in db [:audio :playback-status] status))) +(defn audio-update + "Reacts to audio events fired by the HTML5 audio player and plays the next + track if necessary." + [{:keys [db]} [_ status]] + (cond-> {:db (assoc-in db [:audio :playback-status] status)} + (:ended? status) (assoc :dispatch [::next-song]))) + +(re-frame/reg-event-fx :audio/update audio-update) ;; --- ;; routing diff --git a/src/cljs/airsonic_ui/views.cljs b/src/cljs/airsonic_ui/views.cljs index 8b88a1d..d483633 100644 --- a/src/cljs/airsonic_ui/views.cljs +++ b/src/cljs/airsonic_ui/views.cljs @@ -6,7 +6,7 @@ [airsonic-ui.views.notifications :refer [notification-list]] [airsonic-ui.views.breadcrumbs :refer [breadcrumbs]] - [airsonic-ui.views.bottom-bar :refer [bottom-bar]] + [airsonic-ui.views.audio-player :refer [audio-player]] [airsonic-ui.views.login :refer [login-form]] [airsonic-ui.views.album :as album] [airsonic-ui.views.song :as song])) @@ -61,7 +61,7 @@ ::routes/main [most-recent content] ::routes/artist-view [artist-detail content] ::routes/album-view [album-detail content])]]] - [bottom-bar]])) + [audio-player]])) (defn main-panel [] (let [notifications @(subscribe [::subs/notifications]) diff --git a/src/cljs/airsonic_ui/views/audio_player.cljs b/src/cljs/airsonic_ui/views/audio_player.cljs new file mode 100644 index 0000000..c5ad06a --- /dev/null +++ b/src/cljs/airsonic_ui/views/audio_player.cljs @@ -0,0 +1,77 @@ +(ns airsonic-ui.views.audio-player + (:require [re-frame.core :refer [subscribe]] + [airsonic-ui.utils.helpers :refer [dispatch]] + [airsonic-ui.events :as events] + [airsonic-ui.views.cover :refer [cover]] + [airsonic-ui.views.icon :refer [icon]])) + +;; currently playing / coming next / audio controls... + +(defn current-song-info [song status] + [:article + [:div (:artist song) " - " (:title song)] + ;; FIXME: Sometimes items don't have a duration + [:progress.progress.is-tiny {:value (:current-time status) + :max (:duration song)}]]) + +(defn song-controls [is-playing?] + [:div.field.has-addons + (let [buttons [[:media-step-backward ::events/previous-song] + [(if is-playing? :media-pause :media-play) ::events/toggle-play-pause] + [:media-step-forward ::events/next-song]]] + (map (fn [[icon-glyph event]] + ^{:key icon-glyph} [:p.control>button.button.is-light + {:on-click (dispatch [event])} + [icon icon-glyph]]) + buttons))]) + +(defn- add-classes + "Adds one or more classes to a hiccup keyword" + [elem & classes] + (keyword (apply str (name elem) (->> (filter identity classes) + (map #(str "." (name %))))))) + +(defn- toggle-shuffle [playback-mode] + (dispatch [::events/set-playback-mode (if (= playback-mode :shuffled) + :linear :shuffled)])) + +(defn- advance-repeat-mode [current-mode] + (let [modes (cycle '(:repeat-none :repeat-all :repeat-single)) + next-mode (->> (drop-while (partial not= current-mode) modes) + (second))] + (dispatch [::events/set-repeat-mode next-mode]))) + +(defn playback-mode-controls [playlist] + (let [{:keys [repeat-mode playback-mode]} playlist + button :p.control>button.button.is-light + shuffle-button (add-classes button (when (= playback-mode :shuffled) :is-primary)) + repeat-button (add-classes button (case repeat-mode + :repeat-single :is-info + :repeat-all :is-primary + nil))] + [:div.field.has-addons + ^{:key :shuffle-button} [shuffle-button {:on-click (toggle-shuffle playback-mode)} [icon :random]] + ^{:key :repeat-button} [repeat-button {:on-click (advance-repeat-mode repeat-mode)} [icon :loop]]])) + +(def logo-url "https://airsonic.github.io/airsonic-ui/assets/images/logo/airsonic-light-350x100.png") + +(defn audio-player [] + (let [current-song @(subscribe [:audio/current-song]) + playlist @(subscribe [:audio/playlist]) + playback-status @(subscribe [:audio/playback-status]) + is-playing? @(subscribe [:audio/is-playing?])] + [:nav.navbar.is-fixed-bottom.playback-area + [:div.navbar-brand + [:div.navbar-item + [:img {:src logo-url}]]] + [:div.navbar-menu.is-active + (if current-song + ;; show song info + [:section.level.audio-interaction + [:div.level-left>article.media + [:div.media-left [cover current-song 48]] + [:div.media-content [current-song-info current-song playback-status]]] + [:div.level-right [song-controls is-playing?]] + [:div.level-right [playback-mode-controls playlist]]] + ;; not playing anything + [:p.idle-notification "Currently no song selected"])]])) diff --git a/src/cljs/airsonic_ui/views/bottom_bar.cljs b/src/cljs/airsonic_ui/views/bottom_bar.cljs deleted file mode 100644 index 8e14324..0000000 --- a/src/cljs/airsonic_ui/views/bottom_bar.cljs +++ /dev/null @@ -1,46 +0,0 @@ -(ns airsonic-ui.views.bottom-bar - (:require [re-frame.core :refer [dispatch subscribe]] - [airsonic-ui.events :as events] - [airsonic-ui.views.cover :refer [cover]] - [airsonic-ui.views.icon :refer [icon]])) - -;; currently playing / coming next / audio controls... - -(defn current-song-info [song status] - [:article - [:div (:artist song) " - " (:title song)] - ;; FIXME: Sometimes items don't have a duration - [:progress.progress.is-tiny {:value (:current-time status) - :max (:duration song)}]]) - -(defn playback-controls [is-playing?] - [:div.field.has-addons - (let [buttons [[:media-step-backward ::events/previous-song] - [(if is-playing? :media-pause :media-play) ::events/toggle-play-pause] - [:media-step-forward ::events/next-song]]] - (map (fn [[icon-glyph event]] - ^{:key icon-glyph} [:p.control>button.button.is-light - {:on-click #(dispatch [event])} - [icon icon-glyph]]) - buttons))]) - -(def logo-url "https://airsonic.github.io/airsonic-ui/assets/images/logo/airsonic-light-350x100.png") - -(defn bottom-bar [] - (let [current-song @(subscribe [:audio/current-song]) - playback-status @(subscribe [:audio/playback-status]) - is-playing? @(subscribe [:audio/is-playing?])] - [:nav.navbar.is-fixed-bottom.playback-area - [:div.navbar-brand - [:div.navbar-item - [:img {:src logo-url}]]] - [:div.navbar-menu.is-active - (if current-song - ;; show song info - [:section.level.audio-interaction - [:div.level-left>article.media - [:div.media-left [cover current-song 48]] - [:div.media-content [current-song-info current-song playback-status]]] - [:div.level-right [playback-controls is-playing?]]] - ;; not playing anything - [:p.idle-notification "Currently no song selected"])]])) diff --git a/src/cljs/airsonic_ui/views/song.cljs b/src/cljs/airsonic_ui/views/song.cljs index b8de619..43183f5 100644 --- a/src/cljs/airsonic_ui/views/song.cljs +++ b/src/cljs/airsonic_ui/views/song.cljs @@ -28,4 +28,4 @@ [:td>a {:title "Play last" :href "#" :on-click (dispatch [::events/enqueue-last song])} - [icon :arrow-thick-right]]])]) + [icon :caret-right]]])]) diff --git a/test/cljs/airsonic_ui/events_test.cljs b/test/cljs/airsonic_ui/events_test.cljs index 804150e..1d193b3 100644 --- a/test/cljs/airsonic_ui/events_test.cljs +++ b/test/cljs/airsonic_ui/events_test.cljs @@ -136,3 +136,8 @@ (testing "Should automatically remove a message after a while" (let [fx (events/show-notification {} [:_ :info "This is a notification"])] (is (= :notification/hide (-> (:dispatch-later fx) first :dispatch first)))))) + +(deftest song-has-ended + (testing "Should play the next song when current song has ended" + (is (not (dispatches? (events/audio-update {} [:audio/update {:ended? false}]) ::events/next-song)))) + (is (dispatches? (events/audio-update {} [:audio/update {:ended? true}]) ::events/next-song)))