diff --git a/src/cljs/airsonic_ui/components/current_queue/subs.cljs b/src/cljs/airsonic_ui/components/current_queue/subs.cljs new file mode 100644 index 0000000..d153aac --- /dev/null +++ b/src/cljs/airsonic_ui/components/current_queue/subs.cljs @@ -0,0 +1,15 @@ +(ns airsonic-ui.components.current-queue.subs + (:require [re-frame.core :as rf])) + +(defn queue-info [playlist] + {:count (count playlist) + :duration + (reduce (fn [acc [_ item]] + (+ acc (:duration item))) 0 (:items playlist))}) + +(println "registering the sub") + +(rf/reg-sub + :current-queue/info + :<- [:audio/current-playlist] + queue-info) diff --git a/src/cljs/airsonic_ui/components/current_queue/views.cljs b/src/cljs/airsonic_ui/components/current_queue/views.cljs index 34f71f4..a6bbdca 100644 --- a/src/cljs/airsonic_ui/components/current_queue/views.cljs +++ b/src/cljs/airsonic_ui/components/current_queue/views.cljs @@ -3,9 +3,11 @@ [reagent.core :as r] ["react-sortable-hoc" :refer [SortableHandle]] [bulma.icon :refer [icon]] + [bulma.dropdown.views :refer [dropdown]] [airsonic-ui.helpers :as helpers] [airsonic-ui.components.sortable.views :as sortable] - [airsonic-ui.routes :as routes])) + [airsonic-ui.routes :as routes] + [airsonic-ui.components.current-queue.subs])) (def SortHandle (SortableHandle. @@ -14,24 +16,14 @@ ;; to return React elements from the component. (fn [] (r/as-element [:span.is-size-7.has-text-grey-lighter - [icon :elevator]])))) + [icon :elevator]])))) (defn song-actions [] - (let [controls-id (str "song-actions-" (random-uuid)) - is-active? (r/atom false)] - (fn [] - [(if @is-active? :div.dropdown.is-right.is-active :div.dropdown.is-right) - [:div.dropdown-trigger - [:span.is-small.button {:aria-haspopup "true" - :aria-controls controls-id - :on-click #(swap! is-active? not)} - [icon :ellipses]]] - [:div.dropdown-menu {:id controls-id, :role "menu"} - [:div.dropdown-content - ;; TODO: Implement removal - [:a.dropdown-item {:href "#"} "Remove from queue"] - ;; TODO: Implement "Go to source" - [:a.dropdown-item {:href "#"} "Go to source"]]]]))) + ;; TODO: Implement both of these + [dropdown {:items [{:label "Remove from queue" + :event []} + {:label "Go to source" + :event []}]}]) (defn artist-link [{id :artistId, artist :artist}] (if id @@ -44,14 +36,14 @@ :on-click (helpers/muted-dispatch [:audio-player/set-current-song idx])} (:title song)]) -(defn song-table [{:keys [songs current-song]}] +(defn song-table [{:keys [songs current-song-idx]}] [:table.song-listing-table.table.is-fullwidth [:thead>tr [:td.is-narrow] [:td.song-artist "Artist"] [:td.song-title "Title"] [:td.song-duration "Duration"] - [:td.is-narrow]] + [:td.song-actions.is-narrow]] [sortable/sortable-component {:items songs :container [:tbody] @@ -59,7 +51,7 @@ :render-item (fn [{[idx song] :value}] - [(if (= (:id song) (:id current-song)) :tr.is-playing :tr) + [(if (= idx current-song-idx) :tr.is-playing :tr) [:td.sortable-handle.is-narrow [:> SortHandle]] [:td.song-artist [artist-link song]] [:td.song-title [song-link song idx]] @@ -68,16 +60,32 @@ :on-sort-end (fn [{:keys [old-idx new-idx]}] - (println "moving from" old-idx "to" new-idx) (dispatch [:audio-player/move-song old-idx new-idx]))}]]) +(defn collection-info [{:keys [playlist-info]}] + [:ul.is-smaller.collection-info + [:li [icon :audio-spectrum] (str (:count playlist-info) + (if (pos? (:count playlist-info)) + " tracks" + " track"))] + [:li [icon :clock] (helpers/format-duration (:duration playlist-info))]]) + +(defn playlist [props] + [:div + [collection-info props] + [song-table {:songs (get-in props [:current-playlist :items]) + :current-song-idx (get-in props [:current-playlist :current-idx])}]]) + +(defn empty-playlist [] + [:p "You are currently not playing anything. Use the search or go to your " + [:a {:href (routes/url-for ::routes/library)} "Library"] " to start playing some music."]) + (defn current-queue [] - [:section.section>div.container - [:h1.title "Current Queue"] - (let [current-playlist @(subscribe [:audio/current-playlist]) - current-song @(subscribe [:audio/current-song])] + (let [current-playlist @(subscribe [:audio/current-playlist]) + playlist-info @(subscribe [:current-queue/info])] + [:section.section>div.container + [:h1.title "Current Queue"] (if (empty? current-playlist) - [:p "You are currently not playing anything. Use the search or go to your " - [:a {:href (routes/url-for ::routes/library)} "Library"] " to start playing some music."] - [song-table {:songs (:items current-playlist) - :current-song current-song}]))]) + [empty-playlist] + [playlist {:current-playlist current-playlist + :playlist-info playlist-info}])])) diff --git a/src/cljs/bulma/dropdown/events.cljs b/src/cljs/bulma/dropdown/events.cljs new file mode 100644 index 0000000..7b5f0aa --- /dev/null +++ b/src/cljs/bulma/dropdown/events.cljs @@ -0,0 +1,20 @@ +(ns bulma.dropdown.events + (:require [re-frame.core :as rf])) + +(defn show-dropdown [db [_ dropdown-id]] + (assoc-in db [:bulma :visible-dropdown] dropdown-id)) + +(rf/reg-event-db ::show show-dropdown) + +(defn hide-dropdown [db _] + (update db :bulma dissoc :visible-dropdown)) + +(rf/reg-event-db ::hide hide-dropdown) + +(defn toggle-dropdown [db [_ dropdown-id]] + (let [visible-dropdown (get-in db [:bulma :visible-dropdown])] + (if (= visible-dropdown dropdown-id) + (hide-dropdown db [::hide]) + (show-dropdown db [::show dropdown-id])))) + +(rf/reg-event-db ::toggle toggle-dropdown) diff --git a/src/cljs/bulma/dropdown/subs.cljs b/src/cljs/bulma/dropdown/subs.cljs new file mode 100644 index 0000000..cdeab23 --- /dev/null +++ b/src/cljs/bulma/dropdown/subs.cljs @@ -0,0 +1,22 @@ +(ns bulma.dropdown.subs + (:require [re-frame.core :as rf])) + +;; NOTE: This is almost the same as bulma.modal.subs +;; Maybe we can provide some abstraction that covers both, but maybe we shouldn't + +(defn visible-dropdown + "Gives us the ID of the currently visible dropdown" + [db _] + (get-in db [:bulma :visible-dropdown])) + +(rf/reg-sub ::visible-dropdown visible-dropdown) + +(defn visible? + "Predicate to check the visibility of a single modal" + [visible-dropdown [_ dropdown-id]] + (= visible-dropdown dropdown-id)) + +(rf/reg-sub + ::visible? + :<- [::visible-dropdown] + visible?) diff --git a/src/cljs/bulma/dropdown/views.cljs b/src/cljs/bulma/dropdown/views.cljs new file mode 100644 index 0000000..0d4e2f8 --- /dev/null +++ b/src/cljs/bulma/dropdown/views.cljs @@ -0,0 +1,45 @@ +(ns bulma.dropdown.views + (:require [re-frame.core :refer [dispatch subscribe]] + [reagent.core :as r] + [bulma.icon :refer [icon]] + [bulma.dropdown.events :as ev] + [bulma.dropdown.subs :as sub])) + +;; there's "muted-dispatch" in airsonic-ui.helpers which does the same thing +;; it's not used here because all the bulma.*-components should work independently + +(defn muted-dispatch [event-vector] + (fn [e] + (.preventDefault e) + (dispatch event-vector))) + +(defn generate-id [] + (str "bulma-dropdown-" (random-uuid))) + +(defn click-overlay + [] + [:div {:style {:position "fixed" + :z-index 19 ;; <- 20 is the z-index of .dropdown-menu + :top 0 + :left 0 + :bottom 0 + :right 0} + :on-click #(dispatch [::ev/hide])}]) + +(defn dropdown [{:keys [items]}] + (let [dropdown-id (generate-id)] + (fn [] + (let [visible? @(subscribe [::sub/visible? dropdown-id])] + [(if visible? :div.dropdown.is-right.is-active :div.dropdown.is-right) + (when visible? [click-overlay]) + [:div.dropdown-trigger + [:span.is-small.button {:aria-haspopup "true" + :aria-controls dropdown-id + :on-click #(dispatch [::ev/toggle dropdown-id])} + [icon :ellipses]]] + [:div.dropdown-menu {:id dropdown-id, :role "menu"} + [:div.dropdown-content + (for [[idx {:keys [label event]}] (map-indexed vector items)] + ^{:key (str dropdown-id "-" idx)} + [:a.dropdown-item {:href "#" + :on-click (muted-dispatch event)} label])]]])))) diff --git a/test/cljs/bulma/dropdown_test.cljs b/test/cljs/bulma/dropdown_test.cljs new file mode 100644 index 0000000..005daa1 --- /dev/null +++ b/test/cljs/bulma/dropdown_test.cljs @@ -0,0 +1,40 @@ +(ns bulma.dropdown-test + (:require [cljs.test :refer-macros [deftest testing is]] + [bulma.dropdown.subs :as sub] + [bulma.dropdown.events :as ev])) + +;; NOTE: Here as well; this code is very much like the modal code +;; Not sure whether to explicitly duplicate it or provide some smarter +;; abstraction that's harder to understand at first sight + +(enable-console-print!) + +(deftest bulma-dropdowns + (testing "Should create a collection of dropdowns if there is none" + (let [new-db (ev/show-dropdown {} [::ev/show :some-dropdown-id])] + (is (= :some-dropdown-id (sub/visible-dropdown new-db [::sub/visible-dropdown]))))) + (testing "Should hide other dropdowns when displaying a new one" + (let [dropdown-ids [:some-id-1 :some-id-2 :some-id-3] + new-db (reduce (fn [db dropdown-id] + (ev/show-dropdown db [::ev/show dropdown-id])) + {} dropdown-ids)] + (is (= :some-id-3 (sub/visible-dropdown new-db [::sub/visible-dropdown]))))) + (testing "Should remove a dropdown from the collection when we hide it" + (let [dropdown-ids [:some-id-1 :some-id-2 :some-id-3] + new-db (-> (reduce (fn [db dropdown-id] + (ev/show-dropdown db [::ev/show dropdown-id])) + {} dropdown-ids) + (ev/hide-dropdown [::ev/hide]))] + (is (not (some? (sub/visible-dropdown new-db [::sub/visible-dropdown])))))) + (testing "Should tell us about the visibility of a dropdown with a predicate" + (is (true? (-> (ev/show-dropdown {} [::ev/show :getting-repetitive]) + (sub/visible-dropdown [::sub/visible-dropdown]) + (sub/visible? [::sub/visible? :getting-repetitive]))))) + (testing "Dropdown toggling" + (is (true? (-> (ev/toggle-dropdown {} [::ev/toggle :some-generic-dropdown]) + (sub/visible-dropdown [::sub/visible-dropdown]) + (sub/visible? [::sub/visible? :some-generic-dropdown])))) + (is (not (true? (-> (ev/toggle-dropdown {} [::ev/toggle :some-generic-dropdown]) + (ev/toggle-dropdown [::ev/toggle :some-generic-dropdown]) + (sub/visible-dropdown [::sub/visible-dropdown]) + (sub/visible? [::sub/visible? :some-generic-dropdown])))))))