mirror of
https://github.com/heyarne/airsonic-ui.git
synced 2026-05-06 18:33:38 +02:00
parent
ec4504e475
commit
cefdcd542e
9 changed files with 133 additions and 43 deletions
|
|
@ -11,7 +11,8 @@
|
||||||
(reg-sub :api/responses responses)
|
(reg-sub :api/responses responses)
|
||||||
|
|
||||||
(defn response-for
|
(defn response-for
|
||||||
"Returns the cached response for a single endpoint"
|
"Returns the cached response for a single endpoint, respecting passed
|
||||||
|
url pramters."
|
||||||
[responses [_ endpoint params]]
|
[responses [_ endpoint params]]
|
||||||
(get responses [endpoint params]))
|
(get responses [endpoint params]))
|
||||||
|
|
||||||
|
|
@ -20,9 +21,20 @@
|
||||||
:<- [:api/responses]
|
:<- [:api/responses]
|
||||||
response-for)
|
response-for)
|
||||||
|
|
||||||
|
(defn responses-for-endpoint
|
||||||
|
"Returns a seq of all responses for an endpoint, ignoring url parameters and
|
||||||
|
looking only at the path"
|
||||||
|
[responses [_ endpoint]]
|
||||||
|
(into {} (filter (fn [[[k _] _]] (= endpoint k)) responses)))
|
||||||
|
|
||||||
|
(reg-sub
|
||||||
|
:api/responses-for-endpoint
|
||||||
|
:<- [:api/responses]
|
||||||
|
responses-for-endpoint)
|
||||||
|
|
||||||
(defn endpoint->kw
|
(defn endpoint->kw
|
||||||
"Given an endpoint like `getAlbumList2`, returns a cleaned keyword like
|
"Given an endpoint like `getAlbumList2`, returns a cleaned keyword like
|
||||||
`:album-list``.
|
`:album-list`.
|
||||||
|
|
||||||
Rules: Kebab-case everything, remove prefixes like `get`, `create`, `delete`,
|
Rules: Kebab-case everything, remove prefixes like `get`, `create`, `delete`,
|
||||||
`update` and strip trailing numbers."
|
`update` and strip trailing numbers."
|
||||||
|
|
|
||||||
17
src/cljs/airsonic_ui/components/library/subs.cljs
Normal file
17
src/cljs/airsonic_ui/components/library/subs.cljs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
(ns airsonic-ui.components.library.subs
|
||||||
|
(:require [re-frame.core :as re-frame]
|
||||||
|
[airsonic-ui.config :as conf]))
|
||||||
|
|
||||||
|
(defn complete-library
|
||||||
|
"Concatenates all responses of one type of library to make paging through
|
||||||
|
it a bit easier."
|
||||||
|
[responses [_ kind]]
|
||||||
|
(->> (filter (fn [[[_ params] _]]
|
||||||
|
(= kind (:type params))) responses)
|
||||||
|
(sort-by (fn [[[_ params] _]] (:offset params)))
|
||||||
|
(mapcat (fn [[_ vals]] (:album vals)))))
|
||||||
|
|
||||||
|
(re-frame/reg-sub
|
||||||
|
:library/complete
|
||||||
|
:<- [:api/responses-for-endpoint "getAlbumList2"]
|
||||||
|
complete-library)
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
(ns airsonic-ui.components.library.views
|
(ns airsonic-ui.components.library.views
|
||||||
(:require [airsonic-ui.routes :as routes :refer [url-for]]
|
(:require [re-frame.core :refer [subscribe]]
|
||||||
|
[airsonic-ui.config :as conf]
|
||||||
|
[airsonic-ui.routes :as routes :refer [url-for]]
|
||||||
[airsonic-ui.components.collection.views :as collection]
|
[airsonic-ui.components.collection.views :as collection]
|
||||||
[airsonic-ui.helpers :refer [add-classes]]))
|
[airsonic-ui.helpers :refer [add-classes]]))
|
||||||
|
|
||||||
|
|
@ -11,41 +13,71 @@
|
||||||
{:class-name "is-active"})
|
{:class-name "is-active"})
|
||||||
[:a {:href (apply url-for route)} label]]))]])
|
[:a {:href (apply url-for route)} label]]))]])
|
||||||
|
|
||||||
|
;; the pagination should be used like this
|
||||||
|
;; [pagination {:per-page 12
|
||||||
|
;; :max-pages nil
|
||||||
|
;; :url-fn generate-url
|
||||||
|
;; :current-page 0
|
||||||
|
;; :items [,,,]
|
||||||
|
;; :on-change (fn [current-page items]
|
||||||
|
;; (reset! current-items items))}]
|
||||||
|
|
||||||
|
(defn num-pages [items per-page max-pages]
|
||||||
|
(min (Math/ceil (/ (count items) per-page)) max-pages))
|
||||||
|
|
||||||
(defn pagination
|
(defn pagination
|
||||||
"Builds a pagination, calling `url-fn` for every rendered page link with the
|
"Builds a pagination, calling `url-fn` for every rendered page link with the
|
||||||
page as its argument. When `max-pages` is `nil` an infinite pagination
|
page as its argument. When `max-pages` is `nil` an infinite pagination
|
||||||
will be rendered."
|
will be rendered."
|
||||||
[{:keys [url-fn max-pages current-page]}]
|
[{:keys [items per-page max-pages current-page url-fn on-change]
|
||||||
|
:or {max-pages (.-MAX_VALUE js/Number)}}]
|
||||||
|
(let [num-pages (num-pages items per-page max-pages)
|
||||||
|
first-page? (= current-page 1)
|
||||||
|
last-page? (= current-page num-pages)]
|
||||||
|
(println "range"
|
||||||
|
(count items)
|
||||||
|
"num-pages"
|
||||||
|
num-pages)
|
||||||
[:nav.pagination {:role "pagination", :aria-label "pagination"}
|
[:nav.pagination {:role "pagination", :aria-label "pagination"}
|
||||||
[:a.pagination-previous (if (> current-page 1)
|
[:a.pagination-previous (if first-page?
|
||||||
{:href (url-fn (dec current-page))}
|
{:disabled true}
|
||||||
{:disabled true}) "Previous page"]
|
{:href (url-fn (dec current-page))}) "Previous page"]
|
||||||
[:a.pagination-next (if (= max-pages current-page)
|
[:a.pagination-next (if last-page?
|
||||||
{:disabled true}
|
{:disabled true}
|
||||||
{:href (url-fn (inc current-page))}) "Next page"]
|
{:href (url-fn (inc current-page))}) "Next page"]
|
||||||
[:ul.pagination-list
|
[:ul.pagination-list
|
||||||
(when (> current-page 3)
|
(when (> current-page 3)
|
||||||
^{:key "ellipsis-before"} [:li>span.pagination-ellipsis "…"])
|
^{:key "ellipsis-before"} [:li>span.pagination-ellipsis "…"])
|
||||||
(for [page (range (max 1 (- current-page 2))
|
(for [page (range (max 1 (- current-page 2))
|
||||||
(if max-pages
|
(min (+ current-page 3) (inc num-pages)))]
|
||||||
(min (+ current-page 3) (inc max-pages))
|
|
||||||
(+ current-page 3)))]
|
|
||||||
(let [current-page? (= page current-page)]
|
(let [current-page? (= page current-page)]
|
||||||
^{:key page} [(cond-> :li>a.pagination-link
|
^{:key page} [(cond-> :li>a.pagination-link
|
||||||
current-page? (add-classes :is-current))
|
current-page? (add-classes :is-current))
|
||||||
(cond-> {:href (url-fn page), :aria-label (str "Page " page)}
|
(cond-> {:href (url-fn page), :aria-label (str "Page " page)}
|
||||||
(= page current-page) (assoc :aria-current "page")) page]))
|
(= page current-page) (assoc :aria-current "page")) page]))
|
||||||
(when (or (not max-pages) (< current-page (- max-pages 2)))
|
(when (< current-page (- num-pages 2))
|
||||||
^{:key "ellipsis-after"} [:li>span.pagination-ellipsis "…"])]])
|
^{:key "ellipsis-after"} [:li>span.pagination-ellipsis "…"])]]))
|
||||||
|
|
||||||
(defn main [route {:keys [scan-status album-list]}]
|
(def tab-items [[[::routes/library {:kind "recent"} nil] "Recently played"]
|
||||||
(let [[_ {:keys [criteria]} {:keys [page] :or {page 1}}] route
|
[[::routes/library {:kind "newest"} nil] "Newest additions"]
|
||||||
tab-items [[[::routes/library {:criteria "recent"} nil] "Recently played"]
|
[[::routes/library {:kind "starred"} nil] "Starred"]])
|
||||||
[[::routes/library {:criteria "newest"} nil] "Newest additions"]
|
|
||||||
[[::routes/library {:criteria "starred"} nil] "Starred"]]
|
(defn main
|
||||||
|
"Renders the pagination and shows a list of all albums with their cover art.
|
||||||
|
The first parameter is the route that's passed in, the second one is the
|
||||||
|
content that has been fetched for that route."
|
||||||
|
[[_ {:keys [kind]} {:keys [page]
|
||||||
|
:or {page 1}}]
|
||||||
|
{:keys [scan-status]}]
|
||||||
|
(let [library @(subscribe [:library/complete kind])
|
||||||
|
;; FIXME: vv Views shouldn't do calculations vv
|
||||||
|
visible (->> (drop (* (dec page) conf/albums-per-page) library)
|
||||||
|
(take conf/albums-per-page))
|
||||||
|
url-fn #(url-for ::routes/library {:kind kind} {:page %})
|
||||||
pagination [pagination {:current-page (int page)
|
pagination [pagination {:current-page (int page)
|
||||||
:max-pages 5
|
:per-page conf/albums-per-page
|
||||||
:url-fn #(url-for ::routes/library {:criteria criteria} {:page %})}]]
|
:items library
|
||||||
|
:url-fn url-fn}]]
|
||||||
[:div
|
[:div
|
||||||
[:section.hero.is-small>div.hero-body>div.container
|
[:section.hero.is-small>div.hero-body>div.container
|
||||||
[:h2.title "Your library"]
|
[:h2.title "Your library"]
|
||||||
|
|
@ -54,7 +86,7 @@
|
||||||
(when (:scanning scan-status)
|
(when (:scanning scan-status)
|
||||||
[:p.subtitle.is-5.has-text-grey "Scanning…"]))]
|
[:p.subtitle.is-5.has-text-grey "Scanning…"]))]
|
||||||
[:section.section>div.container
|
[:section.section>div.container
|
||||||
[tabs {:items tab-items :active-item {:criteria criteria}}]
|
[tabs {:items tab-items :active-item {:kind kind}}]
|
||||||
pagination
|
pagination
|
||||||
[:section.section [collection/listing (:album album-list)]]
|
[:section.section [collection/listing visible]]
|
||||||
pagination]]))
|
pagination]]))
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,6 @@
|
||||||
|
|
||||||
(def debug?
|
(def debug?
|
||||||
^boolean goog.DEBUG)
|
^boolean goog.DEBUG)
|
||||||
|
|
||||||
|
;; how many covers are shown per page when browsing the library
|
||||||
|
(def albums-per-page 20)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
[airsonic-ui.api.events]
|
[airsonic-ui.api.events]
|
||||||
[airsonic-ui.api.subs]
|
[airsonic-ui.api.subs]
|
||||||
[airsonic-ui.components.audio-player.events]
|
[airsonic-ui.components.audio-player.events]
|
||||||
|
[airsonic-ui.components.library.subs]
|
||||||
[airsonic-ui.components.search.events]
|
[airsonic-ui.components.search.events]
|
||||||
[airsonic-ui.components.search.subs]
|
[airsonic-ui.components.search.subs]
|
||||||
[airsonic-ui.events :as events]
|
[airsonic-ui.events :as events]
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
(ns airsonic-ui.routes
|
(ns airsonic-ui.routes
|
||||||
(:require [bide.core :as r]
|
(:require [bide.core :as r]
|
||||||
[cljs.reader :refer [read-string]]
|
[cljs.reader :refer [read-string]]
|
||||||
[re-frame.core :as re-frame]))
|
[re-frame.core :as re-frame]
|
||||||
|
[airsonic-ui.config :as conf]))
|
||||||
|
|
||||||
(def default-route ::login)
|
(def default-route ::login)
|
||||||
|
|
||||||
(defonce router
|
(defonce router
|
||||||
(r/router [["/" ::login]
|
(r/router [["/" ::login]
|
||||||
["/library" ::library]
|
["/library" ::library]
|
||||||
["/library/:criteria" ::library]
|
["/library/:kind" ::library]
|
||||||
["/artists" ::artist.overview]
|
["/artists" ::artist.overview]
|
||||||
["/artists/:id" ::artist.detail]
|
["/artists/:id" ::artist.detail]
|
||||||
["/album/:id" ::album.detail]
|
["/album/:id" ::album.detail]
|
||||||
|
|
@ -42,11 +43,15 @@
|
||||||
(defmethod -route-events :default [route-id params query])
|
(defmethod -route-events :default [route-id params query])
|
||||||
|
|
||||||
(defmethod -route-events ::library
|
(defmethod -route-events ::library
|
||||||
[route-id {:keys [criteria]} {:keys [page]}]
|
[route-id {:keys [kind]} {:keys [page] :or {page 1}}]
|
||||||
(if criteria
|
(if kind
|
||||||
[[:api/request "getScanStatus"]
|
[[:api/request "getScanStatus"]
|
||||||
[:api/request "getAlbumList2" {:type criteria, :size 20, :offset (* 20 (dec page))}]]
|
;; we fetch more than just the albums needed for the current page so we can
|
||||||
[:routes/do-navigation [route-id {:criteria "recent"} {:page 1}]]))
|
;; page through it faster
|
||||||
|
[:api/request "getAlbumList2" {:type kind
|
||||||
|
:size (* 3 conf/albums-per-page)
|
||||||
|
:offset (* page conf/albums-per-page)}]]
|
||||||
|
[:routes/do-navigation [route-id {:kind "recent"} {:page 1}]]))
|
||||||
|
|
||||||
(defmethod -route-events ::artist.overview
|
(defmethod -route-events ::artist.overview
|
||||||
[route-id params query]
|
[route-id params query]
|
||||||
|
|
|
||||||
|
|
@ -71,9 +71,9 @@
|
||||||
:title "Current queue"} [icon :audio-spectrum]]
|
:title "Current queue"} [icon :audio-spectrum]]
|
||||||
(when stream-role
|
(when stream-role
|
||||||
[navbar-dropdown "Library"
|
[navbar-dropdown "Library"
|
||||||
[[{:href (url-for ::routes/library {:criteria "recent"})} "Recently played"]
|
[[{:href (url-for ::routes/library {:kind "recent"})} "Recently played"]
|
||||||
[{:href (url-for ::routes/library {:criteria "newest"})} "Newest additions"]
|
[{:href (url-for ::routes/library {:kind "newest"})} "Newest additions"]
|
||||||
[{:href (url-for ::routes/library {:criteria "starred"})} "Starred"]
|
[{:href (url-for ::routes/library {:kind "starred"})} "Starred"]
|
||||||
[{:href (url-for ::routes/artist.overview)} "By artist"]]])
|
[{:href (url-for ::routes/artist.overview)} "By artist"]]])
|
||||||
(when podcast-role
|
(when podcast-role
|
||||||
#_(let [podcast-url (url-for ::routes/podcast.overview)]
|
#_(let [podcast-url (url-for ::routes/podcast.overview)]
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,14 @@
|
||||||
(is (= :result (sub/response-for responses [:api/response-for "search2" {:query "query term"}])))
|
(is (= :result (sub/response-for responses [:api/response-for "search2" {:query "query term"}])))
|
||||||
(is (nil? (sub/response-for responses [:api/response-for "search2" {:query "another query term"}]))))))
|
(is (nil? (sub/response-for responses [:api/response-for "search2" {:query "another query term"}]))))))
|
||||||
|
|
||||||
|
(deftest responses-for-endpoint
|
||||||
|
(testing "Should concatenate all responses for an endpoint"
|
||||||
|
(let [responses {["search2" {:query "query term"}] :result1
|
||||||
|
["something-else" nil] :ignored-result
|
||||||
|
["search2" {:query "another query term"}] :result2}]
|
||||||
|
(is (= (dissoc responses ["something-else" nil])
|
||||||
|
(sub/responses-for-endpoint responses [:api/responses-for-endpoint "search2"]))))))
|
||||||
|
|
||||||
(deftest endpoint-keywordification
|
(deftest endpoint-keywordification
|
||||||
(testing "Should strip prefixes"
|
(testing "Should strip prefixes"
|
||||||
(is (= :artist-info (sub/endpoint->kw "getArtistInfo")))
|
(is (= :artist-info (sub/endpoint->kw "getArtistInfo")))
|
||||||
|
|
|
||||||
12
test/cljs/airsonic_ui/components/library/subs_test.cljs
Normal file
12
test/cljs/airsonic_ui/components/library/subs_test.cljs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
(ns airsonic-ui.components.library.subs-test
|
||||||
|
(:require [cljs.test :refer-macros [deftest testing is]]
|
||||||
|
[airsonic-ui.components.library.subs :as sub]))
|
||||||
|
|
||||||
|
(def responses {["getAlbumList2" {:type "recent" :offset 1}] {:album '(5 6 7 8)}
|
||||||
|
["getAlbumList2" {:type "recent" :offset 0}] {:album '(1 2 3 4)}
|
||||||
|
["getAlbumList2" {:type "newest" :offset 1}] {:album '(9 8 7 6)}})
|
||||||
|
|
||||||
|
(deftest complete-library
|
||||||
|
(testing "Should concatenate all album list responses for a given type of list"
|
||||||
|
(is (= '(1 2 3 4 5 6 7 8)
|
||||||
|
(sub/complete-library responses [:library/complete "recent"])))))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue