mirror of
https://github.com/heyarne/airsonic-ui.git
synced 2026-05-07 02:33:39 +02:00
Merge incomplete podcast support
commit 4ac35d6530f7770e7b80307321c72541a55e2c8e
Author: Arne Schlüter <arne@schlueter.is>
Date: Mon Oct 8 21:09:04 2018 +0200
Stub out podcast detail view
commit 60742a22e93bfe6f432e06d56d3e4da671184559
Author: Arne Schlüter <arne@schlueter.is>
Date: Tue Sep 18 23:02:39 2018 +0200
Simplify api helpers; closes #16
commit 8bbc79ebf4dbbe3dbfa08cb4c7c1edd341d507eb
Author: Arne Schlüter <arne@schlueter.is>
Date: Tue Sep 18 19:39:17 2018 +0200
Adjust `stream-url` to work with podcast episodes
commit 991ba5b65230a7429c160ca1b7968ecbb8595e0b
Author: Arne Schlüter <arne@schlueter.is>
Date: Tue Sep 18 19:14:08 2018 +0200
Fix breadcrumbs for podcasts
commit 37c3a894eded2fe37f9af031d3132c7175702266
Author: Arne Schlüter <arne@schlueter.is>
Date: Tue Sep 18 15:11:54 2018 +0200
Stub out overview for podcasts
This commit is contained in:
parent
38eea1c8c9
commit
fa485bbf42
19 changed files with 350 additions and 133 deletions
|
|
@ -6,12 +6,6 @@
|
|||
[ajax.core :as ajax]
|
||||
[airsonic-ui.api.helpers :as api]))
|
||||
|
||||
(defn- api-url
|
||||
"Small helper function which makes constructing API URLs a bit easier"
|
||||
[db endpoint params]
|
||||
(let [creds (:credentials db)]
|
||||
(api/url (:server creds) endpoint (merge params (select-keys creds [:u :p])))))
|
||||
|
||||
(defn- cache-path [endpoint params] [:api/responses [endpoint params]])
|
||||
|
||||
(defn api-request
|
||||
|
|
@ -19,7 +13,7 @@
|
|||
current app state."
|
||||
[{:keys [db]} [_ endpoint params]]
|
||||
{:http-xhrio {:method :get
|
||||
:uri (api-url db endpoint params)
|
||||
:uri (api/url (:credentials db) endpoint params)
|
||||
:response-format (ajax/json-response-format {:keywords? true})
|
||||
:on-success [:api/good-response endpoint params]
|
||||
:on-failure [:api/failed-response endpoint params]}
|
||||
|
|
|
|||
|
|
@ -6,22 +6,25 @@
|
|||
:c "airsonic-ui-cljs"
|
||||
:v "1.15.0"})
|
||||
|
||||
(defn- encode [c]
|
||||
(js/encodeURIComponent c))
|
||||
(def ^:private encode js/encodeURIComponent)
|
||||
|
||||
(defn url
|
||||
"Returns an absolute url to an API endpoint"
|
||||
[server endpoint params]
|
||||
(let [query (->> (merge default-params params)
|
||||
[credentials endpoint params]
|
||||
(let [server (:server credentials)
|
||||
query (->> (merge default-params (select-keys credentials [:u :p]) params)
|
||||
(map (fn [[k v]] (str (encode (name k)) "=" (encode v))))
|
||||
(str/join "&"))]
|
||||
(str server (when-not (str/ends-with? server "/") "/") "rest/" endpoint "?" query)))
|
||||
|
||||
(defn song-url [server credentials song]
|
||||
(url server "stream" (merge (select-keys song [:id]) credentials)))
|
||||
(defn stream-url [credentials song-or-episode]
|
||||
;; podcasts have a stream-id, normal songs just use their id
|
||||
(let [params {:id (or (:streamId song-or-episode)
|
||||
(:id song-or-episode))}]
|
||||
(url credentials "stream" params)))
|
||||
|
||||
(defn cover-url [server credentials item size]
|
||||
(url server "getCoverArt" (merge {:id (:coverArt item) :size size} credentials)))
|
||||
(defn cover-url [credentials item size]
|
||||
(url credentials "getCoverArt" {:id (:coverArt item) :size size}))
|
||||
|
||||
(defn is-error? [response]
|
||||
(= "failed" (get-in response [:subsonic-response :status])))
|
||||
|
|
|
|||
|
|
@ -28,12 +28,12 @@
|
|||
|
||||
(re-frame/reg-fx
|
||||
:audio/play
|
||||
(fn [song-url]
|
||||
(fn [stream-url]
|
||||
(when-not @audio
|
||||
(reset! audio (js/Audio.))
|
||||
(attach-listeners! @audio))
|
||||
(.pause @audio)
|
||||
(set! (.-src @audio) song-url)
|
||||
(set! (.-src @audio) stream-url)
|
||||
(.play @audio)))
|
||||
|
||||
(re-frame/reg-fx
|
||||
|
|
|
|||
|
|
@ -3,17 +3,13 @@
|
|||
[airsonic-ui.audio.playlist :as playlist]
|
||||
[airsonic-ui.api.helpers :as api]))
|
||||
|
||||
(defn- song-url [db song]
|
||||
(let [creds (:credentials db)]
|
||||
(api/song-url (:server creds) (select-keys creds [:u :p]) song)))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
; sets up the db, starts to play a song and adds the rest to a playlist
|
||||
:audio-player/play-all
|
||||
(fn [{:keys [db]} [_ songs start-idx]]
|
||||
(let [playlist (-> (playlist/->playlist songs :playback-mode :linear :repeat-mode :repeat-all)
|
||||
(playlist/set-current-song start-idx))]
|
||||
{:audio/play (song-url db (playlist/peek playlist))
|
||||
{:audio/play (api/stream-url (:credentials db) (playlist/peek playlist))
|
||||
:db (assoc-in db [:audio :playlist] playlist)})))
|
||||
|
||||
;; FIXME: :audio/play might not get the right argument here
|
||||
|
|
@ -34,7 +30,7 @@
|
|||
(let [db (update-in db [:audio :playlist] playlist/next-song)
|
||||
next (playlist/peek (get-in db [:audio :playlist]))]
|
||||
{:db db
|
||||
:audio/play (song-url db next)})))
|
||||
:audio/play (api/stream-url (:credentials db) next)})))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
:audio-player/previous-song
|
||||
|
|
@ -42,7 +38,7 @@
|
|||
(let [db (update-in db [:audio :playlist] playlist/previous-song)
|
||||
prev (playlist/peek (get-in db [:audio :playlist]))]
|
||||
{:db db
|
||||
:audio/play (song-url db prev)})))
|
||||
:audio/play (api/stream-url (:credentials db) prev)})))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
:audio-player/enqueue-next
|
||||
|
|
|
|||
|
|
@ -22,17 +22,17 @@
|
|||
year (conj [:li [icon :calendar] (str "Released in " year)]))))
|
||||
|
||||
|
||||
(defn preview [album]
|
||||
(defn album-card [album]
|
||||
(let [{:keys [artist artistId name id]} album]
|
||||
[card album
|
||||
:url-fn #(url-for ::routes/album-view {:id id})
|
||||
:url-fn #(url-for ::routes/album.detail {:id id})
|
||||
:content [:div
|
||||
;; link to album
|
||||
[:div.title.is-5
|
||||
[:a {:href (url-for ::routes/album-view {:id id})
|
||||
[:a {:href (url-for ::routes/album.detail {:id id})
|
||||
:title name} name]]
|
||||
;; link to artist page
|
||||
[:div.subtitle.is-6 [:a {:href (url-for ::routes/artist-view {:id artistId})
|
||||
[:div.subtitle.is-6 [:a {:href (url-for ::routes/artist.detail {:id artistId})
|
||||
:title artist} artist]]]]))
|
||||
|
||||
(defn listing [albums]
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
[:div.columns.is-multiline.is-mobile
|
||||
(for [[idx album] (map-indexed vector albums)]
|
||||
^{:key idx} [:div.column.is-one-fifth-desktop.is-one-quarter-tablet.is-half-mobile
|
||||
[preview album]])])
|
||||
[album-card album]])])
|
||||
|
||||
(defn detail
|
||||
"Lists all songs in an album"
|
||||
|
|
|
|||
13
src/cljs/airsonic_ui/components/podcast/events.cljs
Normal file
13
src/cljs/airsonic_ui/components/podcast/events.cljs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
(ns airsonic-ui.components.podcast.events)
|
||||
|
||||
(defn subscribe-to-channel
|
||||
[db [_ channel-url]])
|
||||
|
||||
(defn delete-podcast-channel
|
||||
[db [_ channel-id]])
|
||||
|
||||
(defn download-episode
|
||||
[db [_ episode-id]])
|
||||
|
||||
(defn delete-episode
|
||||
[db [_ episode-id]])
|
||||
52
src/cljs/airsonic_ui/components/podcast/subs.cljs
Normal file
52
src/cljs/airsonic_ui/components/podcast/subs.cljs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
(ns airsonic-ui.components.podcast.subs
|
||||
(:require [re-frame.core :refer [reg-sub]]))
|
||||
|
||||
;; this unwraps the api response into a collection
|
||||
(reg-sub
|
||||
::podcast.response
|
||||
:<- [:api/response-for "getPodcasts"]
|
||||
(fn [response]
|
||||
(:channel response)))
|
||||
|
||||
(defn podcast-channels
|
||||
"Given a podcast response, returns information about the channels that have
|
||||
been subscribed to."
|
||||
[response _]
|
||||
(when response
|
||||
(map #(dissoc % :episode) response)))
|
||||
|
||||
(reg-sub
|
||||
::podcast.channels
|
||||
:<- [::podcast.response]
|
||||
podcast-channels)
|
||||
|
||||
(defn sorted-podcast-episodes
|
||||
"Given a response of all podcasts, returns all episodes sorted by a given function"
|
||||
[response [_ key-fn & {:keys [n reverse?]
|
||||
:or {n 15
|
||||
reverse? true}}]]
|
||||
;; some podcasts have an :artist and some don't, we make sure all of them have one
|
||||
(let [id->channel (into {} (map (juxt :id :title) response))]
|
||||
(let [sorted (->> (mapcat :episode response)
|
||||
(map (fn [episode]
|
||||
(assoc episode :artist (id->channel (:channelId episode)))))
|
||||
(sort-by (or key-fn identity)))]
|
||||
(take n (if reverse? (reverse sorted) sorted)))))
|
||||
|
||||
(reg-sub
|
||||
::podcast.all-episodes-by
|
||||
:<- [::podcast.response]
|
||||
sorted-podcast-episodes)
|
||||
|
||||
(defn podcast-detail
|
||||
"Since there's no real detail request, this function provides some abstraction
|
||||
for that in providing a lense to only the podcast with a specific channel-id."
|
||||
[[response [_ params _]] _]
|
||||
(let [channel-id (:id params)]
|
||||
(first (filter #(= channel-id (:id %)) response))))
|
||||
|
||||
(reg-sub
|
||||
::podcast.detail-from-route
|
||||
:<- [::podcast.response]
|
||||
:<- [:routes/current-route]
|
||||
podcast-detail)
|
||||
83
src/cljs/airsonic_ui/components/podcast/views.cljs
Normal file
83
src/cljs/airsonic_ui/components/podcast/views.cljs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
(ns airsonic-ui.components.podcast.views
|
||||
(:require [re-frame.core :refer [subscribe]]
|
||||
[airsonic-ui.helpers :refer [muted-dispatch]]
|
||||
[airsonic-ui.routes :as routes :refer [url-for]]
|
||||
[airsonic-ui.components.podcast.subs :as subs]
|
||||
[airsonic-ui.views.cover :refer [cover card]]
|
||||
[airsonic-ui.views.icon :refer [icon]]
|
||||
[airsonic-ui.components.debug.views :refer [debug]]))
|
||||
|
||||
;; TODO: Implement detail pages for podcasts
|
||||
;; TODO: Implement CRUD frontend for podcasts
|
||||
;; TODO: Error handling for channels and episodes
|
||||
|
||||
(defn channel-card
|
||||
"Displays the cover of a podcast and links to the podcasts detail page"
|
||||
[channel]
|
||||
[card channel
|
||||
:url-fn #(url-for ::routes/podcast.detail {:id (:id channel)})
|
||||
:content [:div.title.is-5
|
||||
[:a {:href (url-for ::routes/podcast.detail {:id (:id channel)})
|
||||
:title (:title channel)} (:title channel)]]])
|
||||
(defn- channel-overview [channels]
|
||||
[:div.columns.is-multiline.is-mobile
|
||||
(for [[idx channel] (map-indexed vector channels)]
|
||||
^{:key idx}
|
||||
[:div.column.is-one-fifth-desktop.is-one-quarter-tablet.is-half-mobile
|
||||
[channel-card channel]])])
|
||||
|
||||
(defn- episode-actions [episode]
|
||||
(case (:status episode)
|
||||
"completed"
|
||||
[[:td>a {:title "Play next"
|
||||
:href "#"
|
||||
:on-click (muted-dispatch [:audio-player/enqueue-next episode])}
|
||||
[icon :plus]]
|
||||
[:td>a {:title "Play last"
|
||||
:href "#"
|
||||
:on-click (muted-dispatch [:audio-player/enqueue-last episode])}
|
||||
[icon :caret-right]]]
|
||||
"skipped" ;; FIXME: Show download button
|
||||
[[:td] [:td]]))
|
||||
|
||||
(defn- episode-list [episodes]
|
||||
[:table.table.is-striped.is-hoverable.is-fullwidth>tbody
|
||||
(for [[idx episode] (map-indexed vector episodes)]
|
||||
^{:key idx}
|
||||
(into
|
||||
[:tr
|
||||
[:td.grow [:span
|
||||
[:a {:href (url-for ::routes/podcast.detail {:id (:channelId episode)})}
|
||||
(:artist episode)]
|
||||
" - "
|
||||
[:a {:title (:title episode)
|
||||
:href "#"
|
||||
:on-click (muted-dispatch [:audio-player/play-all episodes idx])}
|
||||
(:title episode)]]]]
|
||||
(episode-actions episode)))])
|
||||
|
||||
(defn detail
|
||||
"Detail page for a single channel"
|
||||
[_]
|
||||
;; NOTE: This isn't especially pretty, but it works. The detail page can only
|
||||
;; ever be displayed for the podcast the current route points to
|
||||
(let [channel @(subscribe [::subs/podcast.detail-from-route])]
|
||||
[:div
|
||||
[:section.section>div.hero-body
|
||||
[:div.container>article.media
|
||||
[:div.media-left [cover channel 128]]
|
||||
[:div.media-content
|
||||
[:h2.title (:title channel)]
|
||||
[:p (:description channel)]]]]
|
||||
[:section.section>div.container [episode-list (:episode channel)]]]))
|
||||
|
||||
(defn overview
|
||||
"All channels and most recently published shows"
|
||||
[_]
|
||||
(let [channels @(subscribe [::subs/podcast.channels])
|
||||
episodes @(subscribe [::subs/podcast.all-episodes-by :created])]
|
||||
[:section.section>div.container
|
||||
[:h1.title "Subscriptions"]
|
||||
[channel-overview channels]
|
||||
[:h1.title "Latest Episodes"]
|
||||
[episode-list episodes]]))
|
||||
|
|
@ -19,10 +19,11 @@
|
|||
:default-value search-term
|
||||
:placeholder "Search"}]]])))
|
||||
|
||||
|
||||
(defn artist-results [{:keys [artist]}]
|
||||
[:div.columns.is-multiline.is-mobile
|
||||
(for [[idx artist] (map-indexed vector artist)]
|
||||
(let [url #(url-for ::routes/artist-view (select-keys % [:id]))]
|
||||
(let [url #(url-for ::routes/artist.detail (select-keys % [:id]))]
|
||||
^{:key idx} [:div.column.is-2
|
||||
[card artist
|
||||
:url-fn url
|
||||
|
|
@ -33,7 +34,7 @@
|
|||
(defn album-results [{:keys [album]}]
|
||||
[:div.columns.is-multiline.is-mobile
|
||||
(for [[idx album] (map-indexed vector album)]
|
||||
(let [url #(url-for ::routes/album-view (select-keys % [:id]))
|
||||
(let [url #(url-for ::routes/album.detail (select-keys % [:id]))
|
||||
title (str (:name album) " (" (:artist album) ")")]
|
||||
^{:key idx} [:div.column.is-2 [card album
|
||||
:url-fn url
|
||||
|
|
|
|||
|
|
@ -67,9 +67,7 @@
|
|||
the credentials when the request was successful."
|
||||
[cofx [_ credentials]]
|
||||
(assoc cofx :http-xhrio {:method :get
|
||||
:uri (api/url (:server credentials) "getUser"
|
||||
(merge (select-keys credentials [:u :p])
|
||||
{:username (:u credentials)}))
|
||||
:uri (api/url credentials "getUser" {:username (:u credentials)})
|
||||
:response-format (ajax/json-response-format {:keywords? true})
|
||||
: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
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@
|
|||
(r/router [["/" ::login]
|
||||
["/library" ::library]
|
||||
["/library/:criteria" ::library]
|
||||
["/artist/:id" ::artist-view]
|
||||
["/album/:id" ::album-view]
|
||||
["/search" ::search]]))
|
||||
["/artist/:id" ::artist.detail]
|
||||
["/album/:id" ::album.detail]
|
||||
["/search" ::search]
|
||||
["/podcast" ::podcast.overview]
|
||||
["/podcast/:id" ::podcast.detail]]))
|
||||
|
||||
;; use this in views to construct a url
|
||||
(defn url-for
|
||||
|
|
@ -20,15 +22,20 @@
|
|||
([k params query] (str "#" (r/resolve router k params query))))
|
||||
|
||||
;; which routes need valid login credentials?
|
||||
(def protected-routes #{::library ::artist-view ::album-view ::search})
|
||||
(def protected-routes #{::library ::artist.detail ::album.detail ::search
|
||||
::podcast.overview ::podcast.detail})
|
||||
|
||||
;; which data should be requested for which route? can either be a vector or a function returning a vector
|
||||
|
||||
;; TODO: It's not so nice to have this all so close to the routing logic;
|
||||
;; it would be nicer to abstract this away, so the components themselves
|
||||
;; could tell what kind of events they expect
|
||||
|
||||
(defmulti -route-events
|
||||
"Returns the events that take care of correct data being fetched."
|
||||
(fn [route-id & _] route-id))
|
||||
|
||||
(defmethod -route-events :default [route-id params query] nil)
|
||||
(defmethod -route-events :default [route-id params query])
|
||||
|
||||
(defmethod -route-events ::library
|
||||
[route-id {:keys [criteria]} {:keys [page]}]
|
||||
|
|
@ -37,13 +44,13 @@
|
|||
[:api/request "getAlbumList2" {:type criteria, :size 20, :offset (* 20 (dec page))}]]
|
||||
[:routes/do-navigation [route-id {:criteria "recent"} {:page 1}]]))
|
||||
|
||||
(defmethod -route-events ::artist-view
|
||||
(defmethod -route-events ::artist.detail
|
||||
[route-id params query]
|
||||
(let [params (select-keys params [:id])]
|
||||
[[:api/request "getArtist" params]
|
||||
[:api/request "getArtistInfo2" params]]))
|
||||
|
||||
(defmethod -route-events ::album-view
|
||||
(defmethod -route-events ::album.detail
|
||||
[route-id params query]
|
||||
[:api/request "getAlbum" (select-keys params [:id])])
|
||||
|
||||
|
|
@ -52,6 +59,15 @@
|
|||
[[:search/restore-term-from-param (:query query)]
|
||||
[:api/request "search3" query]])
|
||||
|
||||
(defmethod -route-events ::podcast.overview
|
||||
[route-id params query]
|
||||
[[:api/request "getPodcasts"]])
|
||||
|
||||
(defmethod -route-events ::podcast.detail
|
||||
[route-id params query]
|
||||
;; this is identical to ::podcast.overview on purpose
|
||||
[[:api/request "getPodcasts"]])
|
||||
|
||||
;; shouldn't need to change anything below
|
||||
|
||||
(defn- n-events?
|
||||
|
|
|
|||
|
|
@ -80,12 +80,12 @@
|
|||
(defn cover-url
|
||||
"Provides a convenient way for views to get cover images so they don't have
|
||||
to build them themselves and can live a simple and happy life."
|
||||
[[{:keys [server u p]}] [_ song size]]
|
||||
(api/cover-url server {:u u :p p} song size))
|
||||
[credentials [_ song size]]
|
||||
(api/cover-url credentials song size))
|
||||
|
||||
(reg-sub
|
||||
::cover-url
|
||||
(fn [_ _] [(subscribe [::credentials])])
|
||||
:<- [::credentials]
|
||||
cover-url)
|
||||
|
||||
;; user notifications
|
||||
|
|
|
|||
|
|
@ -15,58 +15,81 @@
|
|||
[airsonic-ui.components.search.views :as search]
|
||||
[airsonic-ui.components.library.views :as library]
|
||||
[airsonic-ui.components.artist.views :as artist]
|
||||
[airsonic-ui.components.collection.views :as collection]))
|
||||
[airsonic-ui.components.collection.views :as collection]
|
||||
[airsonic-ui.components.podcast.views :as podcast]))
|
||||
|
||||
(def logo-url "./img/airsonic-light-350x100.png")
|
||||
|
||||
;; ---
|
||||
;; top navigation
|
||||
;; ---
|
||||
|
||||
(defonce navbar-active? (r/atom false))
|
||||
(def toggle-navbar-active! #(swap! navbar-active? not))
|
||||
|
||||
(defn navbar-item [{:keys [href]} label]
|
||||
[:a.navbar-item {:href href :on-click toggle-navbar-active!} label])
|
||||
|
||||
(defn navbar-dropdown
|
||||
([label items] (navbar-dropdown label {} items))
|
||||
([label label-opts items]
|
||||
[:div.navbar-item.has-dropdown.is-hoverable
|
||||
[:div.navbar-link label-opts label]
|
||||
[:div.navbar-dropdown
|
||||
(for [[idx [opts label]] (map-indexed vector items)]
|
||||
^{:key (str "navbar-dropdown-" idx)}
|
||||
[navbar-item
|
||||
(merge {:on-click toggle-navbar-active!} opts)
|
||||
label])]]))
|
||||
|
||||
(defn navbar-top
|
||||
"Contains search, some navigational links and the logo"
|
||||
[]
|
||||
(let [active? (r/atom false)
|
||||
toggle-active #(swap! active? not)
|
||||
navbar-item (fn navbar-item [{:keys [href]} label]
|
||||
[:a.navbar-item {:href href :on-click toggle-active} label])
|
||||
user @(subscribe [:user/info])
|
||||
(let [user @(subscribe [:user/info])
|
||||
stream-role @(subscribe [:user/roles :stream])
|
||||
podcast-role @(subscribe [:user/roles :podcast])
|
||||
playlist-role @(subscribe [:user/roles :playlist])
|
||||
share-role @(subscribe [:user/roles :share])
|
||||
settings-role @(subscribe [:user/roles :settings])]
|
||||
(fn []
|
||||
[:nav.navbar.is-fixed-top.is-dark {:role "navigation", :aria-label "search and navigation"}
|
||||
;; user is `nil` when we're not logged in, we can hide the extended navigation
|
||||
[:div.navbar-brand
|
||||
[:div.navbar-item>img {:src logo-url}]
|
||||
[:div.navbar-burger.burger {:on-click toggle-active} (repeat 3 [:span])]]
|
||||
(when user
|
||||
[(if @active? :div.navbar-menu.is-active :div.navbar-menu)
|
||||
[:div.navbar-start
|
||||
[:div.navbar-item [search/form]]]
|
||||
[:div.navbar-end
|
||||
(when stream-role
|
||||
[:div.navbar-item.has-dropdown.is-hoverable
|
||||
[:div.navbar-link "Library"]
|
||||
[:div.navbar-dropdown
|
||||
[navbar-item {:href (url-for ::routes/library {:criteria "recent"})} "Recently played"]
|
||||
[navbar-item {:href (url-for ::routes/library {:criteria "newest"})} "Newest additions"]
|
||||
[navbar-item {:href (url-for ::routes/library {:criteria "starred"})} "Starred"]]])
|
||||
(when podcast-role
|
||||
[navbar-item {} "Podcasts"])
|
||||
(when playlist-role
|
||||
[navbar-item {} "Playlists"])
|
||||
(when share-role
|
||||
[navbar-item {} "Shares"])
|
||||
[:div.navbar-item.has-dropdown.is-hoverable
|
||||
[:div.navbar-link "More"]
|
||||
[:div.navbar-dropdown.is-right
|
||||
(when settings-role
|
||||
[navbar-item "Settings"])
|
||||
[:a.navbar-item
|
||||
{:on-click (fn [_]
|
||||
(toggle-active)
|
||||
(dispatch [::events/logout]))
|
||||
:href "#"}
|
||||
(str "Logout (" (:username user) ")")]]]]])])))
|
||||
[:nav.navbar.is-fixed-top.is-dark {:role "navigation", :aria-label "search and navigation"}
|
||||
;; user is `nil` when we're not logged in, we can hide the extended navigation
|
||||
[:div.navbar-brand
|
||||
[:div.navbar-item>img {:src logo-url}]
|
||||
[:div.navbar-burger.burger {:on-click toggle-navbar-active!}
|
||||
(for [idx (range 3)] ^{:key (str "burger-" idx)} [:span])]]
|
||||
(when user
|
||||
[(if @navbar-active? :div.navbar-menu.is-active :div.navbar-menu)
|
||||
[:div.navbar-start
|
||||
[:div.navbar-item [search/form]]]
|
||||
[:div.navbar-end
|
||||
(when stream-role
|
||||
[navbar-dropdown "Library"
|
||||
[[{:href (url-for ::routes/library {:criteria "recent"})} "Recently played"]
|
||||
[{:href (url-for ::routes/library {:criteria "newest"})} "Newest additions"]
|
||||
[{:href (url-for ::routes/library {:criteria "starred"})} "Starred"]]])
|
||||
(when podcast-role
|
||||
(let [podcast-url (url-for ::routes/podcast.overview)]
|
||||
[navbar-dropdown "Podcast" {:href podcast-url}
|
||||
[[{:href podcast-url} "Overview"]]]))
|
||||
(when playlist-role
|
||||
[navbar-item {} "Playlists"])
|
||||
(when share-role
|
||||
[navbar-item {} "Shares"])
|
||||
[:div.navbar-item.has-dropdown.is-hoverable
|
||||
[:div.navbar-link "More"]
|
||||
[:div.navbar-dropdown.is-right
|
||||
(when settings-role
|
||||
[navbar-item {} "Settings"])
|
||||
[:a.navbar-item
|
||||
{:on-click (fn [_]
|
||||
(toggle-navbar-active!)
|
||||
(dispatch [::events/logout]))
|
||||
:href "#"}
|
||||
(str "Logout (" (:username user) ")")]]]]])]))
|
||||
|
||||
;; ---
|
||||
;; this is the section the user mainly interacts with
|
||||
;; ---
|
||||
|
||||
(defn media-content
|
||||
"Provides the complete UI to browse the media library, interact with search
|
||||
|
|
@ -80,12 +103,17 @@
|
|||
[breadcrumbs content]
|
||||
(case route-id
|
||||
::routes/library [library/main [route-id params query] content]
|
||||
::routes/artist-view [artist/detail content]
|
||||
::routes/album-view [collection/detail content]
|
||||
::routes/search [search/results content])]
|
||||
::routes/artist.detail [artist/detail content]
|
||||
::routes/album.detail [collection/detail content]
|
||||
::routes/search [search/results content]
|
||||
::routes/podcast.overview [podcast/overview content]
|
||||
::routes/podcast.detail [podcast/detail content])]
|
||||
[audio-player]]))
|
||||
|
||||
(defn main-panel []
|
||||
(defn main-panel
|
||||
"The outermost wrapper; handles display of the login form if necessary,
|
||||
makes the code in media-content a bit easier to follow"
|
||||
[]
|
||||
(let [notifications @(subscribe [::subs/notifications])
|
||||
is-booting? @(subscribe [::subs/is-booting?])
|
||||
[route-id params query] @(subscribe [:routes/current-route])]
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#{:artist :artist-info} :artist
|
||||
#{:album} :album
|
||||
#{:search} :search
|
||||
#{:podcasts} :podcast
|
||||
:other-content))
|
||||
|
||||
(defn- bulma-breadcrumbs [& items]
|
||||
|
|
@ -26,18 +27,20 @@
|
|||
(defmethod breadcrumbs :default [content]
|
||||
[bulma-breadcrumbs "Start"])
|
||||
|
||||
(def start [(url-for ::routes/library) "Start"])
|
||||
|
||||
(defmethod breadcrumbs :artist [{:keys [artist]}]
|
||||
[bulma-breadcrumbs
|
||||
[(url-for ::routes/library) "Start"]
|
||||
[bulma-breadcrumbs start
|
||||
(:name artist)])
|
||||
|
||||
(defmethod breadcrumbs :album [{:keys [album]}]
|
||||
[bulma-breadcrumbs
|
||||
[(url-for ::routes/library) "Start"]
|
||||
[(url-for ::routes/artist-view {:id (:artistId album)}) (:artist album)]
|
||||
[bulma-breadcrumbs start
|
||||
[(url-for ::routes/artist.detail {:id (:artistId album)}) (:artist album)]
|
||||
(:name album)])
|
||||
|
||||
(defmethod breadcrumbs :search [_]
|
||||
[bulma-breadcrumbs
|
||||
[(url-for ::routes/library) "Start"]
|
||||
"Search"])
|
||||
[bulma-breadcrumbs start "Search"])
|
||||
|
||||
(defmethod breadcrumbs :podcast [{:keys [channel]}]
|
||||
;; TODO: Detail view
|
||||
[bulma-breadcrumbs start "Podcasts"])
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
(let [artist-id (:artistId song)]
|
||||
[:div
|
||||
(if artist-id
|
||||
[:a {:href (url-for ::routes/artist-view {:id artist-id})} (:artist song)]
|
||||
[:a {:href (url-for ::routes/artist.detail {:id artist-id})} (:artist song)]
|
||||
(:artist song))
|
||||
" - "
|
||||
[:a
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue