1
0
Fork 0
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:
Arne Schlüter 2018-10-23 13:51:34 +02:00
commit 1c6c6f17e7
4 changed files with 2922 additions and 66 deletions

View file

@ -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
[responses [_ kind]] "Takes all library responses and returns only the ones matching a specific kind"
(let [sorted-albums (->> (filter (fn [[[_ params] _]] [kind responses]
(= kind (:type params))) 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))) (sort-by (fn [[[_ params] _]] (:offset params)))
(keep (comp :album val)))] (mapcat (fn [[[_ params] {albums :album}]]
;; NOTE: we concatenate this manually to avoid duplication; we have to do (let [start-page (/ (:offset params) conf/albums-per-page)]
;; this because fetch more than conf/albums-per-page per page, otherwise we (zipmap (drop start-page (range))
;; can't know whether to show a link to the next page (partition-all conf/albums-per-page albums)))))
;; FIXME: Somehow (last sorted-albums) is nil when (into (sorted-map))))
(concat (mapcat (partial take conf/albums-per-page) (butlast sorted-albums))
(last sorted-albums)))) ;; `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]]
;; note that we "humanize" the keys, meaning page 1 is the page with offset 0
(->> (partition-responses kind responses)
(map (fn [[k v]] [(inc k) v]))
(into (sorted-map))))
(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)

View file

@ -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]]]))

File diff suppressed because it is too large Load diff

View file

@ -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"])))))