1
0
Fork 0
mirror of https://github.com/heyarne/airsonic-ui.git synced 2026-05-07 02:33:39 +02:00

Move artists into library (#68)

* Use more sensible naming for api responses

* Move artist overview into library; closes #50 and #52

* Fix sass live-reload

* Move editor config out of shadow-cljs.edn
This commit is contained in:
heyarne 2019-12-08 00:56:45 +01:00 committed by GitHub
commit 930bf55390
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 82 additions and 47 deletions

View file

@ -5,15 +5,15 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build:cljs": "shadow-cljs release app", "build:cljs": "shadow-cljs release app",
"build:sass": "node-sass --output-style compressed src/sass/app.sass | postcss -o public/app/style.css", "build:sass": "node-sass --output-style compressed src/sass/app.sass | postcss -o public/app/app.css",
"build": "rm -r public/*; run-p copy:* build:*", "build": "rm -r public/*; run-p copy:* build:*",
"copy:assets": "cp -R src/assets/* public/", "copy:assets": "cp -R src/assets/* public/",
"copy:icons": "cp -R node_modules/open-iconic/font/fonts public", "copy:icons": "cp -R node_modules/open-iconic/font/fonts public",
"deploy": "npm run build && gh-pages -d public -m \"Deploying $(git rev-parse --short HEAD)\"", "deploy": "npm run build && gh-pages -d public -m \"Deploying $(git rev-parse --short HEAD)\"",
"dev:cljs": "shadow-cljs watch app test", "dev:cljs": "shadow-cljs watch app test",
"dev:sass": "npm run build:sass; node-sass -w src/sass/app.sass | postcss -o public/app/style.css", "dev:sass": "node-sass -w src/sass/app.sass -o public/app",
"dev:test": "karma start --reporters notify,progress --auto-watch", "dev:test": "karma start --reporters notify,progress --auto-watch",
"dev": "rm -r public/*; npm-run-all copy:* test:compile -p dev:*", "dev": "rm -r public/*; npm-run-all build:sass copy:* test:compile -p dev:*",
"test": "run-s test:compile test:run", "test": "run-s test:compile test:run",
"test:compile": "shadow-cljs compile test", "test:compile": "shadow-cljs compile test",
"test:run": "karma start --single-run" "test:run": "karma start --single-run"

View file

@ -13,9 +13,7 @@
;; debugging ;; debugging
[day8.re-frame/re-frame-10x "0.4.5"] [day8.re-frame/re-frame-10x "0.4.5"]
#_[day8.re-frame/tracing "0.5.1"] #_[day8.re-frame/tracing "0.5.1"]
[philoskim/debux "0.5.6"] [philoskim/debux "0.5.6"]]
;; for CIDER
[cider/cider-nrepl "0.21.1"]]
:nrepl {:port 9000} :nrepl {:port 9000}

View file

@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Airsonic</title> <title>Airsonic</title>
<link rel="stylesheet" href="./app/style.css"> <link rel="stylesheet" href="./app/app.css">
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="./favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="./favicon-16x16.png">

View file

@ -15,13 +15,13 @@
{:http-xhrio {:method :get {:http-xhrio {:method :get
:uri (api/url (:credentials db) endpoint params) :uri (api/url (:credentials db) endpoint params)
:response-format (ajax/json-response-format {:keywords? true}) :response-format (ajax/json-response-format {:keywords? true})
:on-success [:api/good-response endpoint params] :on-success [:api.response/ok endpoint params]
:on-failure [:api/failed-response endpoint params]} :on-failure [:api.response/failed endpoint params]}
:db (assoc-in db (conj (cache-path endpoint params) :api/is-loading?) true)}) :db (assoc-in db (conj (cache-path endpoint params) :api/is-loading?) true)})
(reg-event-fx :api/request api-request) (reg-event-fx :api/request api-request)
(defn good-api-response (defn api-success
"Handles when the server responded. There could still be an error while "Handles when the server responded. There could still be an error while
processing the request on the server side which we have to account for." processing the request on the server side which we have to account for."
[{:keys [db]} [_ endpoint params response]] [{:keys [db]} [_ endpoint params response]]
@ -32,9 +32,9 @@
{:dispatch [:notification/show :error (api/error-msg e)] {:dispatch [:notification/show :error (api/error-msg e)]
:db (update-in db response-cache dissoc :api/is-loading?)})))) :db (update-in db response-cache dissoc :api/is-loading?)}))))
(reg-event-fx :api/good-response good-api-response) (reg-event-fx :api.response/ok api-success)
(defn failed-api-response (defn api-failure
"Handler for catastrophic failures (network errors and such things)" "Handler for catastrophic failures (network errors and such things)"
[fx [ev endpoint params]] [fx [ev endpoint params]]
(let [response-cache (cons :db (cache-path endpoint params))] (let [response-cache (cons :db (cache-path endpoint params))]
@ -42,4 +42,4 @@
:dispatch [:notification/show :error "Communication with server failed. Check browser logs for details."] :dispatch [:notification/show :error "Communication with server failed. Check browser logs for details."]
:db (update-in fx response-cache dissoc :api/is-loading?)})) :db (update-in fx response-cache dissoc :api/is-loading?)}))
(reg-event-fx :api/failed-response failed-api-response) (reg-event-fx :api.response/failed api-failure)

View file

@ -1,5 +1,6 @@
(ns airsonic-ui.components.artist.views (ns airsonic-ui.components.artist.views
(:require [airsonic-ui.components.collection.views :as collection] (:require [airsonic-ui.components.collection.views :as collection]
[airsonic-ui.components.library.views :as library]
[airsonic-ui.routes :as routes] [airsonic-ui.routes :as routes]
[clojure.string :as str])) [clojure.string :as str]))
@ -68,7 +69,7 @@
(defn overview (defn overview
"Displays the alphabetical listing of all artists along with some additional "Displays the alphabetical listing of all artists along with some additional
information about the collection" information about the collection"
[{:keys [artists]}] [current-route {:keys [artists]}]
(let [artists (:index artists) (let [artists (:index artists)
;; TODO: Calculations in views should be avoided ;; TODO: Calculations in views should be avoided
artists-count (count (mapcat :artist artists)) artists-count (count (mapcat :artist artists))
@ -76,8 +77,9 @@
(map :albumCount) (map :albumCount)
(reduce +))] (reduce +))]
[:div [:div
[:section.hero.is-small>div.hero-body [library/tab-section current-route]
[:section.hero.single-line.is-small>div.hero-body
[:div.container [:div.container
[:h1.title "Artists"] [:h1.title "Artists"]
[:p.subtitle.is-5.has-text-grey [:strong artists-count] " artists in your collection with " [:strong album-count] " albums"]]] [:p.subtitle.is-5.has-text-grey [:strong artists-count] " artists with " [:strong album-count] " albums"]]]
[:section.section>div.container [alphabetical-listing artists]]])) [:section.section>div.container [alphabetical-listing artists]]]))

View file

@ -55,7 +55,8 @@
(->> (->>
[[[::routes/library {:kind "recent"}] "Recently played"] [[[::routes/library {:kind "recent"}] "Recently played"]
[[::routes/library {:kind "newest"}] "Newest additions"] [[::routes/library {:kind "newest"}] "Newest additions"]
[[::routes/library {:kind "starred"}] "Starred"]] [[::routes/library {:kind "starred"}] "Starred"]
[[::routes/artist.overview] "Artists"]]
(map (fn [[[id params :as route] label]] (map (fn [[[id params :as route] label]]
(cond-> {:href (apply routes/url-for route) (cond-> {:href (apply routes/url-for route)
:label label} :label label}
@ -63,12 +64,17 @@
(= (:kind params) (:kind current-params))) (= (:kind params) (:kind current-params)))
(assoc :active? true)))))) (assoc :active? true))))))
(defn tab-section [current-route]
[:section.section.ui-tab-bar.is-small>div.container
[tabs {:items (tab-items current-route)}]])
(defn main (defn main
"Renders the pagination and shows a list of all albums with their cover art. "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 The first parameter is the route that's passed in, the second one is the
content that has been fetched for that route." content that has been fetched for that route."
[[_ {:keys [kind]} {:keys [page] :or {page 1}} :as current-route] [[_ {:keys [kind]} {:keys [page] :or {page 1}} :as current-route]
{:keys [scan-status]}] {:keys [scan-status]}]
(println "scan-status" scan-status)
(let [library @(subscribe [:library/paginated kind]) (let [library @(subscribe [:library/paginated kind])
page (int page) page (int page)
current-items (get library page) current-items (get library page)
@ -77,14 +83,13 @@
:items library :items library
:url-fn url-fn}]] :url-fn url-fn}]]
[:div [:div
[:section.hero.is-small>div.hero-body>div.container [tab-section current-route]
[:section.hero.single-line.is-small>div.hero-body>div.container
[:h2.title "Your library"] [:h2.title "Your library"]
(if (:count scan-status) (if (:count scan-status)
[:p.subtitle.is-5.has-text-grey [:strong (:count scan-status)] " items"] [:p.subtitle.is-5.has-text-grey [:strong (:count scan-status)] " items"]
(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.is-small>div.container
[tabs {:items (tab-items current-route)}]]
[:section.section.is-tiny>div.container pagination-links] [:section.section.is-tiny>div.container pagination-links]
[:section.section.is-tiny>div.container [collection/listing current-items]] [:section.section.is-tiny>div.container [collection/listing current-items]]
[:section.section.is-tiny>div.container pagination-links]])) [:section.section.is-tiny>div.container pagination-links]]))

View file

@ -67,7 +67,7 @@
:uri (api/url credentials "getUser" {:username (:u credentials)}) :uri (api/url credentials "getUser" {:username (:u credentials)})
:response-format (ajax/json-response-format {:keywords? true}) :response-format (ajax/json-response-format {:keywords? true})
:on-success [:credentials/authentication-response credentials] :on-success [:credentials/authentication-response credentials]
:on-failure [:api/failed-response]}}) ; <- we don't need endpoint and params here because the response is not cached :on-failure [:api.response/failed]}}) ; <- we don't need endpoint and params here because the response is not cached
(rf/reg-event-fx :credentials/send-authentication-request authentication-request) (rf/reg-event-fx :credentials/send-authentication-request authentication-request)

View file

@ -63,14 +63,16 @@
[:div.navbar-item [:div.navbar-item
[:a {:href (url-for ::routes/library)} [:img {:src logo-url}]] [:a {:href (url-for ::routes/library)} [:img {:src logo-url}]]
[:div.navbar-burger.burger {:on-click toggle-navbar-active!} [:div.navbar-burger.burger {:on-click toggle-navbar-active!}
(for [idx (range 3)] ^{:key (str "burger-" idx)} [:span])]]] [:span]
[:span]
[:span]]]]
(when user (when user
[(if @navbar-active? :div.navbar-menu.is-active :div.navbar-menu) [(if @navbar-active? :div.navbar-menu.is-active :div.navbar-menu)
[:div.navbar-start [:div.navbar-start
[:div.navbar-item [search/form]]] [:div.navbar-item [search/form]]]
[:div.navbar-end [:div.navbar-end
[:a.navbar-item {:href (url-for ::routes/current-queue) [:a.navbar-item {:href (url-for ::routes/current-queue)
:title "Current queue"} [icon :audio-spectrum]] :title "Current queue"} [:span.heart-beat [icon :audio-spectrum]]]
(when stream-role (when stream-role
[navbar-dropdown "Library" [navbar-dropdown "Library"
[[{:href (url-for ::routes/library {:kind "recent"})} "Recently played"] [[{:href (url-for ::routes/library {:kind "recent"})} "Recently played"]
@ -112,7 +114,7 @@
[breadcrumbs route content] [breadcrumbs route content]
(case route-id (case route-id
::routes/library [library/main route content] ::routes/library [library/main route content]
::routes/artist.overview [artist/overview content] ::routes/artist.overview [artist/overview route content]
::routes/artist.detail [artist/detail content] ::routes/artist.detail [artist/detail content]
::routes/album.detail [collection/detail content] ::routes/album.detail [collection/detail content]
::routes/search [search/results content] ::routes/search [search/results content]

View file

@ -17,36 +17,48 @@
(when content-pending? [:span.loader])]]]])) (when content-pending? [:span.loader])]]]]))
(defmulti breadcrumbs (defmulti breadcrumbs
(fn dispatch-on [[route-id] content] route-id)) ;; the first parameter is always the current route, the second parameter is
;; whatever the subscriptions return as the current content (e.g. album title)
(fn dispatch-on [[route-id] _] route-id))
(defmethod breadcrumbs :default [_ _] (defmethod breadcrumbs :default [_ _]
[bulma-breadcrumbs "Start"]) [bulma-breadcrumbs "Airsonic"])
(def start [(url-for ::routes/library) "Start"]) (defmethod breadcrumbs ::routes/library [[_ params] _]
[bulma-breadcrumbs
[(url-for ::routes/library {:kind "recent"}) "Library"]
(case (:kind params)
"recent" "Recently played"
"newest" "Newest additions"
"starred" "Starred")])
(defmethod breadcrumbs ::routes/artist.overview [_ _] (defmethod breadcrumbs ::routes/artist.overview [_ _]
[bulma-breadcrumbs start "Artists"]) [bulma-breadcrumbs
[(url-for ::routes/library {:kind "recent"}) "Library"]
"Artists"])
(defmethod breadcrumbs ::routes/artist.detail [_ {:keys [artist]}] (defmethod breadcrumbs ::routes/artist.detail [_ {:keys [artist]}]
[bulma-breadcrumbs start [bulma-breadcrumbs
[(url-for ::routes/library {:kind "recent"}) "Library"]
[(url-for ::routes/artist.overview) "Artists"] [(url-for ::routes/artist.overview) "Artists"]
(:name artist)]) (:name artist)])
(defmethod breadcrumbs ::routes/album.detail [_ {:keys [album]}] (defmethod breadcrumbs ::routes/album.detail [_ {:keys [album]}]
[bulma-breadcrumbs start [bulma-breadcrumbs
[(url-for ::routes/library {:kind "recent"}) "Library"]
[(url-for ::routes/artist.overview) "Artists"] [(url-for ::routes/artist.overview) "Artists"]
[(url-for ::routes/artist.detail {:id (:artistId album)}) (:artist album)] [(url-for ::routes/artist.detail {:id (:artistId album)}) (:artist album)]
(:name album)]) (:name album)])
(defmethod breadcrumbs ::routes/search [_ _] (defmethod breadcrumbs ::routes/search [_ _]
[bulma-breadcrumbs start "Search"]) [bulma-breadcrumbs "Search"])
(defmethod breadcrumbs ::routes/podcast.overview [_ _] (defmethod breadcrumbs ::routes/podcast.overview [_ _]
;; TODO: Detail view ;; TODO: Detail view
[bulma-breadcrumbs start "Podcasts"]) [bulma-breadcrumbs "Podcasts"])
(defmethod breadcrumbs ::routes/current-queue [_ _] (defmethod breadcrumbs ::routes/current-queue [_ _]
[bulma-breadcrumbs start "Current Queue"]) [bulma-breadcrumbs "Current Queue"])
(defmethod breadcrumbs ::routes/about [_ _] (defmethod breadcrumbs ::routes/about [_ _]
[bulma-breadcrumbs start "About"]) [bulma-breadcrumbs "About"])

View file

@ -1,7 +1,7 @@
(ns bulma.tabs) (ns bulma.tabs)
(defn tabs [{:keys [items]}] (defn tabs [{:keys [items]}]
[:div.tabs [:div.tabs.is-boxed
[:ul [:ul
(for [[idx {:keys [href label active?]}] (map-indexed vector items)] (for [[idx {:keys [href label active?]}] (map-indexed vector items)]
^{:key idx} [:li (when active? {:class "is-active"}) ^{:key idx} [:li (when active? {:class "is-active"})

View file

@ -228,14 +228,30 @@
&.is-tiny &.is-tiny
padding: 0.75rem 1.5rem padding: 0.75rem 1.5rem
// tab bar on top
&.ui-tab-bar
padding-bottom: 0.75rem
// occurs on many pages at the top to show details // occurs on many pages at the top to show details
.hero .hero
&.is-small + .section &.is-small + .section
padding-top: 0 padding: 1.5rem 1.5rem
&.is-tiny + .section
padding: 0.75rem 1.5rem
.media-content .media-content
align-self: center align-self: center
// modifies our headlines to be next to each other
+tablet
&.single-line .container
display: flex
align-items: baseline
.title
flex-grow: 1
margin-bottom: 0
// floating notifications // floating notifications
.notifications:not(:empty) .notifications:not(:empty)
@extend .container @extend .container

View file

@ -7,7 +7,7 @@
(deftest api-failure-notifcations (deftest api-failure-notifcations
(testing "Should show an error notification when airsonic responds with an error" (testing "Should show an error notification when airsonic responds with an error"
(let [fx (events/good-api-response {} [:api/good-response "ping" nil (:error fixtures/responses)]) (let [fx (events/api-success {} [:api.response/ok "ping" nil (:error fixtures/responses)])
ev (:dispatch fx)] ev (:dispatch fx)]
(is (= :notification/show (first ev))) (is (= :notification/show (first ev)))
(is (= :error (second ev)))))) (is (= :error (second ev))))))
@ -18,13 +18,13 @@
(testing "Should be cached" (testing "Should be cached"
(testing "when the response was successful" (testing "when the response was successful"
(let [endpoint "getScanStatus" (let [endpoint "getScanStatus"
successful (events/good-api-response {} [:api/good-response endpoint nil (:ok fixtures/responses)]) successful (events/api-success {} [:api.response/ok endpoint nil (:ok fixtures/responses)])
unsuccessful (events/good-api-response {} [:api/good-response endpoint nil (:error fixtures/responses)])] unsuccessful (events/api-success {} [:api.response/ok endpoint nil (:error fixtures/responses)])]
(is (map? (cache successful [endpoint]))) (is (map? (cache successful [endpoint])))
(is (nil? (cache unsuccessful [endpoint]))))) (is (nil? (cache unsuccessful [endpoint])))))
(testing "in an unwrapped format" (testing "in an unwrapped format"
(let [endpoint "getScanStatus" (let [endpoint "getScanStatus"
fx (events/good-api-response {} [:api/good-response endpoint nil (:ok fixtures/responses)])] fx (events/api-success {} [:api.response/ok endpoint nil (:ok fixtures/responses)])]
(is (= #{:count :scanning} (set (keys (cache fx [endpoint])))))))) (is (= #{:count :scanning} (set (keys (cache fx [endpoint]))))))))
(testing "When being issued" (testing "When being issued"
(let [endpoint "getScanStatus" (let [endpoint "getScanStatus"
@ -34,16 +34,16 @@
(is (contains? fx :http-xhrio))) (is (contains? fx :http-xhrio)))
(testing "should indicate that a request is ongoing" (testing "should indicate that a request is ongoing"
(is (true? (:api/is-loading? (cache fx [endpoint]))) "for non-cached responses") (is (true? (:api/is-loading? (cache fx [endpoint]))) "for non-cached responses")
(is (true? (-> (events/good-api-response fx [:api/good-response endpoint nil (:ok fixtures/responses)]) (is (true? (-> (events/api-success fx [:api.response/ok endpoint nil (:ok fixtures/responses)])
(events/api-request [:api/request endpoint]) (events/api-request [:api/request endpoint])
(cache [endpoint]) (cache [endpoint])
:api/is-loading?)) "for cached responses")) :api/is-loading?)) "for cached responses"))
(testing "should remove the indication that a request is ongoing when there is a response" (testing "should remove the indication that a request is ongoing when there is a response"
(is (not (:api/is-loading? (-> (events/good-api-response fx [:api/good-response endpoint nil (:ok fixtures/responses)]) (is (not (:api/is-loading? (-> (events/api-success fx [:api.response/ok endpoint nil (:ok fixtures/responses)])
(cache [endpoint])))) "for a good response") (cache [endpoint])))) "for a good response")
(is (not (:api/is-loading? (-> (merge fx (events/good-api-response fx [:api/good-response endpoint nil (:error fixtures/responses)])) (is (not (:api/is-loading? (-> (merge fx (events/api-success fx [:api.response/ok endpoint nil (:error fixtures/responses)]))
(cache [endpoint])))) "when an error is returned") (cache [endpoint])))) "when an error is returned")
(is (not (:api/is-loading? (-> (merge fx (events/failed-api-response fx [:api/failed-response endpoint])) (is (not (:api/is-loading? (-> (merge fx (events/api-failure fx [:api.response/failed endpoint]))
(cache [endpoint])))) "when communication with the server failed")))) (cache [endpoint])))) "when communication with the server failed"))))
(testing "Should be able to avoid the cache" (testing "Should be able to avoid the cache"
;; FIXME: Implement this ;; FIXME: Implement this

View file

@ -62,7 +62,7 @@
(testing "invokes correct callback on server response" (testing "invokes correct callback on server response"
(is (= [:credentials/authentication-response fixtures/credentials] (:on-success request)))) (is (= [:credentials/authentication-response fixtures/credentials] (:on-success request))))
(testing "invokes correct callback when server is not reachable" (testing "invokes correct callback when server is not reachable"
(is (= [:api/failed-response] (:on-failure request)))))) (is (= [:api.response/failed] (:on-failure request))))))
(deftest authentication-response (deftest authentication-response
(testing "On success" (testing "On success"