diff --git a/src/assets/index.html b/src/assets/index.html index 33ff69a..6bb7603 100644 --- a/src/assets/index.html +++ b/src/assets/index.html @@ -1,5 +1,5 @@ - +
diff --git a/src/cljs/airsonic_ui/components/artist/views.cljs b/src/cljs/airsonic_ui/components/artist/views.cljs new file mode 100644 index 0000000..a0b287e --- /dev/null +++ b/src/cljs/airsonic_ui/components/artist/views.cljs @@ -0,0 +1,38 @@ +(ns airsonic-ui.components.artist.views + (:require [airsonic-ui.views.album :as album] + [clojure.string :as str])) + +(defn link-button [attrs children] + [:p.control>a.button.is-small (merge attrs {:target "_blank"}) children]) + +(defn lastfm-bio + "Displays the last.fm biography without the 'Read more on Last.fm' link" + [artist-info] + (when (:biography artist-info) + (let [biography (str/replace (:biography artist-info) #"$" "")] + [:p {:dangerouslySetInnerHTML {:__html biography}}]))) + +(defn lastfm-link [artist-info] + [link-button {:href (:lastFmUrl artist-info)} "See on last.fm"]) + +(defn musicbrainz-link [artist-info] + (let [href (str "https://musicbrainz.org/artist/" (:musicBrainzId artist-info))] + [link-button {:href href} "See on musicbrainz"])) + +(defn detail + "Creates a nice artist page displaying the artist's name, bio (if available and + listing) their albums." + [{:keys [artist artist-info]}] + [:div + [:section.hero>div.hero-body + [:div.container + [:h2.title (:name artist)] + [:div.content + [lastfm-bio artist-info] + (when-not (empty? (select-keys artist-info [:lastFmUrl :musicBrainzId])) + [:div.field.is-grouped + (when (:lastFmUrl artist-info) + [lastfm-link artist-info]) + (when (:musicBrainzId artist-info) + [musicbrainz-link artist-info])])]]] + [:section.section>div.container [album/listing (:album artist)]]]) diff --git a/src/cljs/airsonic_ui/components/audio_player/events.cljs b/src/cljs/airsonic_ui/components/audio_player/events.cljs new file mode 100644 index 0000000..c71bba7 --- /dev/null +++ b/src/cljs/airsonic_ui/components/audio_player/events.cljs @@ -0,0 +1,69 @@ +(ns airsonic-ui.components.audio-player.events + (:require [re-frame.core :as re-frame] + [airsonic-ui.audio.playlist :as playlist] + [airsonic-ui.api.helpers :as api])) + +(defn- song-url [db song] + (let [creds (:credentials db)] + (api/song-url (:server creds) (select-keys creds [:u :p]) song))) + +(re-frame/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]] + (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)) + :db (assoc-in db [:audio :playlist] playlist)}))) + +;; FIXME: :audio/play might not get the right argument here + +(re-frame/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 + :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 + :audio-player/next-song + (fn [{:keys [db]} _] + (let [db (update-in db [:audio :playlist] playlist/next-song) + next (playlist/peek (get-in db [:audio :playlist]))] + {:db db + :audio/play (song-url db next)}))) + +(re-frame/reg-event-fx + :audio-player/previous-song + (fn [{:keys [db]} _] + (let [db (update-in db [:audio :playlist] playlist/previous-song) + prev (playlist/peek (get-in db [:audio :playlist]))] + {:db db + :audio/play (song-url db prev)}))) + +(re-frame/reg-event-db + :audio-player/enqueue-next + (fn [db [_ song]] + (update-in db [:audio :playlist] #(playlist/enqueue-next % song)))) + +(re-frame/reg-event-db + :audio-player/enqueue-last + (fn [db [_ song]] + (update-in db [:audio :playlist] #(playlist/enqueue-last % song)))) + +(re-frame/reg-event-fx + :audio-player/toggle-play-pause + (fn [_ _] + {:audio/toggle-play-pause nil})) + +(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 [:audio-player/next-song]))) + +(re-frame/reg-event-fx :audio/update audio-update) diff --git a/src/cljs/airsonic_ui/views/audio_player.cljs b/src/cljs/airsonic_ui/components/audio_player/views.cljs similarity index 80% rename from src/cljs/airsonic_ui/views/audio_player.cljs rename to src/cljs/airsonic_ui/components/audio_player/views.cljs index 09a66d0..5148f2a 100644 --- a/src/cljs/airsonic_ui/views/audio_player.cljs +++ b/src/cljs/airsonic_ui/components/audio_player/views.cljs @@ -1,7 +1,6 @@ -(ns airsonic-ui.views.audio-player +(ns airsonic-ui.components.audio-player.views (:require [re-frame.core :refer [subscribe]] [airsonic-ui.helpers :refer [add-classes dispatch]] - [airsonic-ui.events :as events] [airsonic-ui.views.cover :refer [cover]] [airsonic-ui.views.icon :refer [icon]])) @@ -16,9 +15,9 @@ (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]]] + (let [buttons [[:media-step-backward :audio-player/previous-song] + [(if is-playing? :media-pause :media-play) :audio-player/toggle-play-pause] + [:media-step-forward :audio-player/next-song]]] (map (fn [[icon-glyph event]] ^{:key icon-glyph} [:p.control>button.button.is-light {:on-click (dispatch [event])} @@ -26,14 +25,14 @@ buttons))]) (defn- toggle-shuffle [playback-mode] - (dispatch [::events/set-playback-mode (if (= playback-mode :shuffled) + (dispatch [:audio-player/set-playback-mode (if (= playback-mode :shuffled) :linear :shuffled)])) (defn- toggle-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]))) + (dispatch [:audio-player/set-repeat-mode next-mode]))) (defn playback-mode-controls [playlist] (let [{:keys [repeat-mode playback-mode]} playlist @@ -47,17 +46,12 @@ ^{:key :shuffle-button} [shuffle-button {:on-click (toggle-shuffle playback-mode)} [icon :random]] ^{:key :repeat-button} [repeat-button {:on-click (toggle-repeat-mode repeat-mode)} [icon :loop]]])) -(def logo-url "./img/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.audio-player - [:div.navbar-brand - [:div.navbar-item - [:img {:src logo-url}]]] [:div.navbar-menu.is-active (if current-song ;; show song info @@ -68,4 +62,4 @@ [:div.level-right [song-controls is-playing?]] [:div.level-right [playback-mode-controls playlist]]] ;; not playing anything - [:p.idle-notification "Currently no song selected"])]])) + [:p.has-text-light.navbar-item.idle-notification "Select a song to start playing"])]])) diff --git a/src/cljs/airsonic_ui/components/collection/views.cljs b/src/cljs/airsonic_ui/components/collection/views.cljs new file mode 100644 index 0000000..a4715f0 --- /dev/null +++ b/src/cljs/airsonic_ui/components/collection/views.cljs @@ -0,0 +1,12 @@ +(ns airsonic-ui.components.collection.views + (:require [airsonic-ui.views.song :as song])) + +(defn detail + "Lists all songs in an album" + [{:keys [album]}] + [:div + [:section.hero>div.hero-body + [:div.container + [:h2.title (:name album)] + [:h3.subtitle (:artist album)]]] + [:section.section>div.container [song/listing (:song album)]]]) diff --git a/src/cljs/airsonic_ui/components/debug/views.cljs b/src/cljs/airsonic_ui/components/debug/views.cljs new file mode 100644 index 0000000..c61b499 --- /dev/null +++ b/src/cljs/airsonic_ui/components/debug/views.cljs @@ -0,0 +1,7 @@ +(ns airsonic-ui.components.debug.views + (:require [clojure.pprint :refer [pprint]])) + +(defn debug + "Returns a nicely formatted debug view of any given data structure" + [data] + [:pre (with-out-str (pprint data))]) diff --git a/src/cljs/airsonic_ui/components/library/views.cljs b/src/cljs/airsonic_ui/components/library/views.cljs index b969824..4b0f0d4 100644 --- a/src/cljs/airsonic_ui/components/library/views.cljs +++ b/src/cljs/airsonic_ui/components/library/views.cljs @@ -16,7 +16,7 @@ page as its argument. When `max-pages` is `nil` an infinite pagination will be rendered." [{:keys [url-fn max-pages current-page]}] - [:nav.pagination.is-centered {:role "pagination", :aria-label "pagination"} + [:nav.pagination {:role "pagination", :aria-label "pagination"} [:a.pagination-previous (if (> current-page 1) {:href (url-fn (dec current-page))} {:disabled true}) "Previous page"] @@ -35,25 +35,26 @@ current-page? (add-classes :is-current)) (cond-> {:href (url-fn page), :aria-label (str "Page " page)} (= page current-page) (assoc :aria-current "page")) page])) - (when (or (not max-pages) (< max-pages (- max-pages 3))) + (when (or (not max-pages) (< current-page (- max-pages 2))) ^{:key "ellipsis-after"} [:li>span.pagination-ellipsis "…"])]]) (defn main [route {:keys [scan-status album-list]}] (let [[_ {:keys [criteria]} {:keys [page] :or {page 1}}] route + tab-items [[[::routes/library {:criteria "recent"} nil] "Recently played"] + [[::routes/library {:criteria "newest"} nil] "Newest additions"] + [[::routes/library {:criteria "starred"} nil] "Starred"]] pagination [pagination {:current-page (int page) :max-pages 5 :url-fn #(url-for ::routes/library {:criteria criteria} {:page %})}]] [:div - [:h2.title "Your library"] - (if (:count scan-status) - [:p.subtitle.is-5.has-text-grey "Containing " [:strong (:count scan-status)] " items"] - (when (:scanning scan-status) - [:p.subtitle.is-5.has-text-grey "Scanning…"])) - (let [items [[[::routes/library {:criteria "recent"} nil] "Recently played"] - [[::routes/library {:criteria "newest"} nil] "Newest additions"] - [[::routes/library {:criteria "starred"} nil] "Starred"]]] - [tabs {:items items :active-item {:criteria criteria}}]) - pagination - [:section.section - [album/listing (:album album-list)]] - pagination])) + [:section.hero.is-small>div.hero-body>div.container + [:h2.title "Your library"] + (if (:count scan-status) + [:p.subtitle.is-5.has-text-grey "Containing " [:strong (:count scan-status)] " items"] + (when (:scanning scan-status) + [:p.subtitle.is-5.has-text-grey "Scanning…"]))] + [:section.section>div.container + [tabs {:items tab-items :active-item {:criteria criteria}}] + pagination + [:section.section [album/listing (:album album-list)]] + pagination]])) diff --git a/src/cljs/airsonic_ui/components/search/views.cljs b/src/cljs/airsonic_ui/components/search/views.cljs index 58a08a2..43d85de 100644 --- a/src/cljs/airsonic_ui/components/search/views.cljs +++ b/src/cljs/airsonic_ui/components/search/views.cljs @@ -1,6 +1,5 @@ (ns airsonic-ui.components.search.views - (:require [clojure.pprint :refer [pprint]] - [re-frame.core :refer [dispatch subscribe]] + (:require [re-frame.core :refer [dispatch subscribe]] [goog.functions :refer [debounce]] [airsonic-ui.routes :as routes :refer [url-for]] [airsonic-ui.views.song :as song] @@ -47,7 +46,7 @@ (defn results [{:keys [search]}] (let [term @(subscribe [:search/current-term])] - [:div + [:section.section>div.container [:h2.title (str "Search results for \"" term "\"")] (if (empty? search) [:p "The server returned no results."] @@ -63,5 +62,4 @@ (when-not (empty? (:song search)) [:section.section.is-small [:h3.subtitle.is-5 "Songs"] - [song-results search]])]) - [:pre (with-out-str (pprint search))]])) + [song-results search]])])])) diff --git a/src/cljs/airsonic_ui/core.cljs b/src/cljs/airsonic_ui/core.cljs index 9728053..d668679 100644 --- a/src/cljs/airsonic_ui/core.cljs +++ b/src/cljs/airsonic_ui/core.cljs @@ -10,6 +10,7 @@ [airsonic-ui.audio.core] [airsonic-ui.api.events] [airsonic-ui.api.subs] + [airsonic-ui.components.audio-player.events] [airsonic-ui.components.search.events] [airsonic-ui.components.search.subs] [airsonic-ui.events :as events] diff --git a/src/cljs/airsonic_ui/events.cljs b/src/cljs/airsonic_ui/events.cljs index 099c6cd..133b507 100644 --- a/src/cljs/airsonic_ui/events.cljs +++ b/src/cljs/airsonic_ui/events.cljs @@ -102,20 +102,11 @@ (re-frame/reg-event-fx :credentials/authentication-success authentication-success) -;; TODO: We have to find another solution for this once we have routes that -;; don't require a login but have the bottom controls - -(re-frame/reg-fx - :show-nav-bar - (fn [_] - (.. js/document -documentElement -classList (add "has-navbar-fixed-bottom")))) - (defn logged-in [cofx _] (let [redirect (or (get-in cofx [:routes/from-query-param :redirect]) [::routes/library])] - {:dispatch [:routes/do-navigation redirect] - :show-nav-bar nil})) + {:dispatch [:routes/do-navigation redirect]})) (re-frame/reg-event-fx ::logged-in @@ -135,77 +126,6 @@ (re-frame/reg-event-fx ::logout logout) -;; --- -;; musique -;; --- - -; TODO: Make play, next and previous a bit prettier and more DRY - -(defn- song-url [db song] - (let [creds (:credentials db)] - (api/song-url (:server creds) (select-keys creds [:u :p]) song))) - -(re-frame/reg-event-fx - ; sets up the db, starts to play a song and adds the rest to a playlist - ::play-songs - (fn [{:keys [db]} [_ songs start-idx]] - (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)) - :db (assoc-in db [:audio :playlist] playlist)}))) - -;; FIXME: :audio/play might not get the right argument here - -(re-frame/reg-event-db - ::set-playback-mode - (fn [db [_ playback-mode]] - (update-in db [:audio :playlist] #(playlist/set-playback-mode % playback-mode)))) - -(re-frame/reg-event-db - ::set-repeat-mode - (fn [db [_ repeat-mode]] - (update-in db [:audio :playlist] #(playlist/set-repeat-mode % repeat-mode)))) - -(re-frame/reg-event-fx - ::next-song - (fn [{:keys [db]} _] - (let [db (update-in db [:audio :playlist] playlist/next-song) - next (playlist/peek (get-in db [:audio :playlist]))] - {:db db - :audio/play (song-url db next)}))) - -(re-frame/reg-event-fx - ::previous-song - (fn [{:keys [db]} _] - (let [db (update-in db [:audio :playlist] playlist/previous-song) - prev (playlist/peek (get-in db [:audio :playlist]))] - {:db db - :audio/play (song-url db prev)}))) - -(re-frame/reg-event-db - ::enqueue-next - (fn [db [_ song]] - (update-in db [:audio :playlist] #(playlist/enqueue-next % song)))) - -(re-frame/reg-event-db - ::enqueue-last - (fn [db [_ song]] - (update-in db [:audio :playlist] #(playlist/enqueue-last % song)))) - -(re-frame/reg-event-fx - ::toggle-play-pause - (fn [_ _] - {:audio/toggle-play-pause nil})) - -(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/subs.cljs b/src/cljs/airsonic_ui/subs.cljs index 6fe9793..b863d9c 100644 --- a/src/cljs/airsonic_ui/subs.cljs +++ b/src/cljs/airsonic_ui/subs.cljs @@ -20,7 +20,7 @@ ::user (fn [_ _] [(subscribe [::credentials])]) (fn [[credentials] _] - {:name (:u credentials)})) + (when credentials {:name (:u credentials)}))) (defn cover-url "Provides a convenient way for views to get cover images so they don't have diff --git a/src/cljs/airsonic_ui/views.cljs b/src/cljs/airsonic_ui/views.cljs index 0fea090..ad198ea 100644 --- a/src/cljs/airsonic_ui/views.cljs +++ b/src/cljs/airsonic_ui/views.cljs @@ -1,4 +1,6 @@ (ns airsonic-ui.views + "This module contains the outmost layer of our app views. It makes sure that + the proper subscriptions are run and arranges the complete layout." (:require [re-frame.core :refer [dispatch subscribe]] [airsonic-ui.routes :as routes :refer [url-for]] [airsonic-ui.events :as events] @@ -7,73 +9,71 @@ [airsonic-ui.views.notifications :refer [notification-list]] [airsonic-ui.views.breadcrumbs :refer [breadcrumbs]] - [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] + [airsonic-ui.components.audio-player.views :refer [audio-player]] [airsonic-ui.components.search.views :as search] - [airsonic-ui.components.library.views :as library])) + [airsonic-ui.components.library.views :as library] + [airsonic-ui.components.artist.views :as artist] + [airsonic-ui.components.collection.views :as collection])) -;; TODO: Find better names and places for these. +(def logo-url "./img/airsonic-light-350x100.png") -(defn album-detail [{:keys [album]}] - [:div - [:h2.title (str (:artist album) " - " (:name album))] - [song/listing (:song album)]]) - -(defn artist-detail [{:keys [artist artist-info]}] - [:div - [:h2.title (:name artist)] - [:div.content>p {:dangerouslySetInnerHTML {:__html (:biography artist-info)}}] - [album/listing (:album artist)]]) - -(defn sidebar [user] - [:aside.menu.section - [search/form] - [:p.menu-label "Music"] - [:ul.menu-list - [:li [:a "By artist"]] - [:li [:a "Top rated"]] - [:li [:a "Most played"]]] - [:p.menu-label "Playlists"] - [:p.menu-label "Shares"] - [:p.menu-label "Podcasts"] - [:p.menu-label "User area"] - [:ul.menu-list - [:li [:a "Settings"]] - [:li [:a +(defn navbar-top + "Contains search, some navigational links and the logo" + [{:keys [user]}] + [:nav.navbar.is-fixed-top.is-dark {:role "navigation", :aria-label "search and navigation"} + [:div.navbar-brand + [:div.navbar-item>img {:src logo-url}]] + ;; user is `nil` when we're not logged in, we can hide the extended navbar + (when user + [:div.navbar-menu + [:div.navbar-start + [:div.navbar-item [search/form]]] + [:div.navbar-end + [:div.navbar-item.has-dropdown.is-hoverable + [:div.navbar-link "Library"] + [:div.navbar-dropdown + [:a.navbar-item {:href (url-for ::routes/library {:criteria "recent"})} "Recently played"] + [:a.navbar-item {:href (url-for ::routes/library {:criteria "newest"})} "Newest additions"] + [:a.navbar-item {:href (url-for ::routes/library {:criteria "starred"})} "Starred"]]] + [:a.navbar-item {} "Podcasts"] + [:a.navbar-item {} "Shares"] + [:div.navbar-item.has-dropdown.is-hoverable + [:div.navbar-link "More"] + [:div.navbar-dropdown.is-right + [:a.navbar-item {:disabled true} "Settings"] + [:a.navbar-item {:on-click #(dispatch [::events/logout]) :href "#"} - (str "Logout (" (:name user) ")")]]]]) + (str "Logout (" (:name user) ")")]]]]])]) -;; putting everything together - -(defn app [route-id params query] - (let [user @(subscribe [::subs/user]) - ;; TODO: Move this to a layer 3 subscription ↓ +(defn media-content + "Provides the complete UI to browse the media library, interact with search + results etc" + [route-id params query] + (let [;; TODO: Move this to a layer 3 subscription ↓ route-events @(subscribe [:routes/events-for-current-route]) content @(subscribe [:api/route-data route-events])] [:div - [:main.columns - [:div.column.is-2.sidebar - [sidebar user]] - [:div.column.is-10 - [:section.section - [breadcrumbs content] - (case route-id - ::routes/library [library/main [route-id params query] content] - ::routes/artist-view [artist-detail content] - ::routes/album-view [album-detail content] - ::routes/search [search/results content])]]] + [:section.section + [breadcrumbs content] + (case route-id + ::routes/library [library/main [route-id params query] content] + ::routes/artist-view [artist/detail content] + ::routes/album-view [collection/detail content] + ::routes/search [search/results content])] [audio-player]])) (defn main-panel [] (let [notifications @(subscribe [::subs/notifications]) is-booting? @(subscribe [::subs/is-booting?]) - [route-id params query] @(subscribe [:routes/current-route])] + [route-id params query] @(subscribe [:routes/current-route]) + user @(subscribe [::subs/user])] [(add-classes :div route-id) [notification-list notifications] (if is-booting? [:div.app-loading>div.loader] - (case route-id - ::routes/login [login-form] - [app route-id params query]))])) + [:div + [navbar-top {:user user}] + (case route-id + ::routes/login [login-form] + [media-content route-id params query])])])) diff --git a/src/cljs/airsonic_ui/views/breadcrumbs.cljs b/src/cljs/airsonic_ui/views/breadcrumbs.cljs index e8f133f..2ed4429 100644 --- a/src/cljs/airsonic_ui/views/breadcrumbs.cljs +++ b/src/cljs/airsonic_ui/views/breadcrumbs.cljs @@ -15,7 +15,7 @@ :other-content)) (defn- bulma-breadcrumbs [& items] - [:nav.breadcrumb {:aria-label "breadcrumbs"} + [:div.container>nav.breadcrumb {:aria-label "breadcrumbs"} [:ul (for [[idx [href label]] (map-indexed vector (butlast items))] [:li {:key idx} [:a {:href href} label]]) diff --git a/src/cljs/airsonic_ui/views/song.cljs b/src/cljs/airsonic_ui/views/song.cljs index 8f675cd..ad1bc66 100644 --- a/src/cljs/airsonic_ui/views/song.cljs +++ b/src/cljs/airsonic_ui/views/song.cljs @@ -1,6 +1,5 @@ (ns airsonic-ui.views.song (:require [airsonic-ui.helpers :refer [dispatch]] - [airsonic-ui.events :as events] [airsonic-ui.routes :as routes :refer [url-for]] [airsonic-ui.views.icon :refer [icon]])) @@ -12,7 +11,7 @@ (:artist song)) " - " [:a - {:href "#" :on-click (dispatch [::events/play-songs songs idx])} + {:href "#" :on-click (dispatch [:audio-player/play-all songs idx])} (:title song)]])) (defn listing [songs] @@ -23,9 +22,9 @@ ;; FIXME: Not implemented yet [:td>a {:title "Play next" :href "#" - :on-click (dispatch [::events/enqueue-next song])} + :on-click (dispatch [:audio-player/enqueue-next song])} [icon :plus]] [:td>a {:title "Play last" :href "#" - :on-click (dispatch [::events/enqueue-last song])} + :on-click (dispatch [:audio-player/enqueue-last song])} [icon :caret-right]]])]) diff --git a/src/sass/app.sass b/src/sass/app.sass index 1ea4dad..793ebc7 100644 --- a/src/sass/app.sass +++ b/src/sass/app.sass @@ -89,7 +89,7 @@ width: 100% // floating notifications -.notifications +.notifications:not(:empty) @extend .container z-index: 100 position: fixed diff --git a/test/cljs/airsonic_ui/components/audio_player/events_test.cljs b/test/cljs/airsonic_ui/components/audio_player/events_test.cljs new file mode 100644 index 0000000..4225bc3 --- /dev/null +++ b/test/cljs/airsonic_ui/components/audio_player/events_test.cljs @@ -0,0 +1,10 @@ +(ns airsonic-ui.components.audio-player.events-test + (:require [cljs.test :refer-macros [deftest testing is]] + [airsonic-ui.test-helpers :refer [dispatches?]] + [airsonic-ui.components.audio-player.events :as events])) + + +(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}]) :audio-player/next-song))) + (is (dispatches? (events/audio-update {} [:audio/update {:ended? true}]) :audio-player/next-song)))) diff --git a/test/cljs/airsonic_ui/events_test.cljs b/test/cljs/airsonic_ui/events_test.cljs index 260e91a..2423af4 100644 --- a/test/cljs/airsonic_ui/events_test.cljs +++ b/test/cljs/airsonic_ui/events_test.cljs @@ -129,8 +129,3 @@ (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))))