mirror of
https://github.com/heyarne/airsonic-ui.git
synced 2026-05-07 10:43:39 +02:00
Improve pagination; no items are repeated, items loaded ahead are kept and many calculations have been moved to subscriptions
This commit is contained in:
parent
bb9bc7e44c
commit
1c6c6f17e7
4 changed files with 2922 additions and 66 deletions
|
|
@ -2,22 +2,40 @@
|
||||||
(:require [re-frame.core :as re-frame]
|
(:require [re-frame.core :as re-frame]
|
||||||
[airsonic-ui.config :as conf]))
|
[airsonic-ui.config :as conf]))
|
||||||
|
|
||||||
(defn complete-library
|
;; first some helper functions to make the structure a bit clearer
|
||||||
"Concatenates all responses of one type of library to make paging through
|
|
||||||
it a bit easier."
|
(defn filter-response-kind
|
||||||
|
"Takes all library responses and returns only the ones matching a specific kind"
|
||||||
|
[kind responses]
|
||||||
|
(filter (fn [[[_ params] _]]
|
||||||
|
(= kind (:type params))) responses))
|
||||||
|
|
||||||
|
(defn partition-responses
|
||||||
|
"Returns a map of responses, where each response is neatly mapped to the page
|
||||||
|
it show on."
|
||||||
|
[kind responses]
|
||||||
|
(->> (filter-response-kind kind responses)
|
||||||
|
(sort-by (fn [[[_ params] _]] (:offset params)))
|
||||||
|
(mapcat (fn [[[_ params] {albums :album}]]
|
||||||
|
(let [start-page (/ (:offset params) conf/albums-per-page)]
|
||||||
|
(zipmap (drop start-page (range))
|
||||||
|
(partition-all conf/albums-per-page albums)))))
|
||||||
|
(into (sorted-map))))
|
||||||
|
|
||||||
|
;; `complete-library` is the subscription that is actually exported
|
||||||
|
|
||||||
|
(defn paginated-library
|
||||||
|
"Returns a sorted map that can be used to access the library content loaded
|
||||||
|
from the server. Each key represents a page and the associated value
|
||||||
|
represents the page's content."
|
||||||
[responses [_ kind]]
|
[responses [_ kind]]
|
||||||
(let [sorted-albums (->> (filter (fn [[[_ params] _]]
|
;; note that we "humanize" the keys, meaning page 1 is the page with offset 0
|
||||||
(= kind (:type params))) responses)
|
(->> (partition-responses kind responses)
|
||||||
(sort-by (fn [[[_ params] _]] (:offset params)))
|
(map (fn [[k v]] [(inc k) v]))
|
||||||
(keep (comp :album val)))]
|
(into (sorted-map))))
|
||||||
;; NOTE: we concatenate this manually to avoid duplication; we have to do
|
|
||||||
;; this because fetch more than conf/albums-per-page per page, otherwise we
|
|
||||||
;; can't know whether to show a link to the next page
|
|
||||||
;; FIXME: Somehow (last sorted-albums) is nil when
|
|
||||||
(concat (mapcat (partial take conf/albums-per-page) (butlast sorted-albums))
|
|
||||||
(last sorted-albums))))
|
|
||||||
|
|
||||||
(re-frame/reg-sub
|
(re-frame/reg-sub
|
||||||
:library/complete
|
:library/paginated
|
||||||
:<- [:api/responses-for-endpoint "getAlbumList2"]
|
:<- [:api/responses-for-endpoint "getAlbumList2"]
|
||||||
complete-library)
|
paginated-library)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,45 +12,43 @@
|
||||||
^{:key idx} [:li (when (= params active-item)
|
^{:key idx} [:li (when (= params active-item)
|
||||||
{:class-name "is-active"})
|
{:class-name "is-active"})
|
||||||
[:a {:href (apply url-for route)} label]]))]])
|
[:a {:href (apply url-for route)} label]]))]])
|
||||||
|
;; this variable determines how many pages before the first known page we should list
|
||||||
;; the pagination should be used like this
|
(def page-padding 2)
|
||||||
;; [pagination {:per-page 12
|
|
||||||
;; :max-pages nil
|
|
||||||
;; :url-fn generate-url
|
|
||||||
;; :current-page 0
|
|
||||||
;; :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 [items per-page max-pages current-page url-fn]
|
[{:keys [items current-page url-fn]}]
|
||||||
:or {max-pages (.-MAX_VALUE js/Number)}}]
|
;; NOTE: This is currently slightly flawed. We don't have any good way to
|
||||||
(let [num-pages (num-pages items per-page max-pages)
|
;; know whether we're on the last possible page so we take the last loaded
|
||||||
|
;; page instead
|
||||||
|
(let [num-pages (last (keys items))
|
||||||
first-page? (= current-page 1)
|
first-page? (= current-page 1)
|
||||||
last-page? (= current-page num-pages)]
|
pages (range (max 1 (- current-page page-padding))
|
||||||
[:nav.pagination {:role "pagination", :aria-label "pagination"}
|
(min (inc (+ current-page page-padding)) (inc num-pages))) ]
|
||||||
|
[:nav.pagination.is-centered {:role "pagination", :aria-label "pagination"}
|
||||||
|
;; now we add buttons to progress one page in each direction
|
||||||
[:a.pagination-previous (if first-page?
|
[:a.pagination-previous (if first-page?
|
||||||
{:disabled true}
|
{:disabled true}
|
||||||
{:href (url-fn (dec current-page))}) "Previous page"]
|
{:href (url-fn (dec current-page))}) "Previous page"]
|
||||||
[:a.pagination-next (if last-page?
|
[:a.pagination-next {:href (url-fn (inc current-page))} "Next page"]
|
||||||
{:disabled true}
|
;; and here we modify the links around our current page
|
||||||
{:href (url-fn (inc current-page))}) "Next page"]
|
|
||||||
[:ul.pagination-list
|
[:ul.pagination-list
|
||||||
(when (> current-page 3)
|
;; some indication that there are previous pages
|
||||||
^{:key "ellipsis-before"} [:li>span.pagination-ellipsis "…"])
|
(when (> current-page (inc page-padding))
|
||||||
(for [page (range (max 1 (- current-page 2))
|
[:li>span.pagination-ellipsis "…"])
|
||||||
(min (+ current-page 3) (inc num-pages)))]
|
;; all pagination links around our current page
|
||||||
|
(for [page pages]
|
||||||
(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)}
|
||||||
current-page? (assoc :aria-current "page")) page]))
|
current-page? (assoc :aria-current "page")) page]))
|
||||||
(when (< current-page (- num-pages 2))
|
;; some indication that there are more pages after
|
||||||
^{:key "ellipsis-after"} [:li>span.pagination-ellipsis "…"])]]))
|
(when (< current-page (- num-pages page-padding))
|
||||||
|
[:li>span.pagination-ellipsis "…"])]]))
|
||||||
|
|
||||||
|
|
||||||
(def tab-items [[[::routes/library {:kind "recent"} nil] "Recently played"]
|
(def tab-items [[[::routes/library {:kind "recent"} nil] "Recently played"]
|
||||||
[[::routes/library {:kind "newest"} nil] "Newest additions"]
|
[[::routes/library {:kind "newest"} nil] "Newest additions"]
|
||||||
|
|
@ -63,13 +61,11 @@
|
||||||
[[_ {:keys [kind]} {:keys [page]
|
[[_ {:keys [kind]} {:keys [page]
|
||||||
:or {page 1}}]
|
:or {page 1}}]
|
||||||
{:keys [scan-status]}]
|
{:keys [scan-status]}]
|
||||||
(let [library @(subscribe [:library/complete kind])
|
(let [page (int page)
|
||||||
;; FIXME: vv Views shouldn't do calculations vv
|
library @(subscribe [:library/paginated kind])
|
||||||
visible (->> (drop (* (dec (int page)) conf/albums-per-page) library)
|
current-items (get library page)
|
||||||
(take conf/albums-per-page))
|
|
||||||
url-fn #(url-for ::routes/library {:kind kind} {:page %})
|
url-fn #(url-for ::routes/library {:kind kind} {:page %})
|
||||||
pagination [pagination {:current-page (int page)
|
pagination [pagination {:current-page page
|
||||||
:per-page conf/albums-per-page
|
|
||||||
:items library
|
:items library
|
||||||
:url-fn url-fn}]]
|
:url-fn url-fn}]]
|
||||||
[:div
|
[:div
|
||||||
|
|
@ -82,5 +78,5 @@
|
||||||
[:section.section>div.container
|
[:section.section>div.container
|
||||||
[tabs {:items tab-items :active-item {:kind kind}}]
|
[tabs {:items tab-items :active-item {:kind kind}}]
|
||||||
pagination
|
pagination
|
||||||
[:section.section [collection/listing visible]]
|
[:section.section [collection/listing current-items]
|
||||||
pagination]]))
|
pagination]]]))
|
||||||
|
|
|
||||||
2834
test/cljs/airsonic_ui/components/library/fixtures.cljs
Normal file
2834
test/cljs/airsonic_ui/components/library/fixtures.cljs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,25 +1,33 @@
|
||||||
(ns airsonic-ui.components.library.subs-test
|
(ns airsonic-ui.components.library.subs-test
|
||||||
(:require [cljs.test :refer-macros [deftest testing is]]
|
(:require [cljs.test :refer-macros [deftest testing is]]
|
||||||
[airsonic-ui.config :as conf]
|
[airsonic-ui.config :as conf]
|
||||||
|
[airsonic-ui.components.library.fixtures :as fixtures]
|
||||||
[airsonic-ui.components.library.subs :as sub]))
|
[airsonic-ui.components.library.subs :as sub]))
|
||||||
|
|
||||||
(defn stub-albums [offset]
|
(deftest partition-library
|
||||||
(let [start (* offset conf/albums-per-page)
|
(testing "Should give us a map of page -> content"
|
||||||
end (inc (+ start (* conf/albums-per-page conf/albums-prefetch-factor)))]
|
(let [pages (sub/partition-responses "recent" fixtures/responses)]
|
||||||
(range start end)))
|
(is (map? pages))
|
||||||
|
(is (every? int? (keys pages)))
|
||||||
|
(is (every? seq? (vals pages)))))
|
||||||
|
(testing "Should map each response correctly to a page"
|
||||||
|
(let [first-response (select-keys fixtures/responses [["getAlbumList2" {:type "recent", :size 100, :offset 0}]])]
|
||||||
|
(is (= (range 5) (keys (sub/partition-responses "recent" first-response)))))
|
||||||
|
(let [first-and-third (select-keys fixtures/responses [["getAlbumList2" {:type "recent", :size 100, :offset 0}]
|
||||||
|
["getAlbumList2" {:type "recent", :size 100, :offset 40}]])]
|
||||||
|
;; there will be overlapping content for pages 2, 3 and 4 (with a zero-based index)
|
||||||
|
(is (= (range 7) (keys (sub/partition-responses "recent" first-and-third)))))))
|
||||||
|
|
||||||
(def responses {["getAlbumList2" {:type "recent" :offset 1}] {:album (stub-albums 1)}
|
(deftest paginated-library
|
||||||
["getAlbumList2" {:type "recent" :offset 2}] {:album (stub-albums 2)}
|
(testing "Should humanize page offsets"
|
||||||
["getAlbumList2" {:type "recent" :offset 0}] {:album (stub-albums 0)}
|
(let [responses (select-keys fixtures/responses [["getAlbumList2" {:type "recent", :size 100, :offset 0}]])
|
||||||
;; vvv this one shouldn't show up in the test vvv
|
paginated (sub/paginated-library responses [:sub/paginated-library "recent"])]
|
||||||
["getAlbumList2" {:type "newest" :offset 1}] {:album (reverse (stub-albums 1))}
|
(is (= [1 2 3 4 5] (keys paginated)))))
|
||||||
["getAlbumList2" {:type "recent" :offset 3}] {:album (stub-albums 3)}})
|
(testing "Should concatenate and deduplicate all album list responses"
|
||||||
|
(let [responses (select-keys fixtures/responses [["getAlbumList2" {:type "recent", :size 100, :offset 0}]
|
||||||
(deftest complete-library
|
["getAlbumList2" {:type "recent", :size 100, :offset 20}]
|
||||||
(testing "Should concatenate and deduplicate all album list responses for a given type of list"
|
["getAlbumList2" {:type "recent", :size 100, :offset 40}]])
|
||||||
;; we test from offset 0 to 3, which is where these numbers come from
|
paginated (sub/paginated-library responses [:sub/paginated-library "recent"])]
|
||||||
(println "last number" (last (stub-albums 3)))
|
(is (= [1 2 3 4 5 6 7] (keys paginated)))
|
||||||
|
(is (= 140 (count (mapcat val paginated))))
|
||||||
(is (= (range 0 (inc (+ (* 3 conf/albums-per-page)
|
(is (= 140 (count (set (mapcat val paginated))))))))
|
||||||
(* conf/albums-per-page conf/albums-prefetch-factor))))
|
|
||||||
(sub/complete-library responses [:library/complete "recent"])))))
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue