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

Move navigation to the top

Squashed commit of the following:

commit b03c1ea7ed0d2fbd56f56f3273e694abc5454101
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed Aug 29 11:38:32 2018 +0200

    Fix bug where dropdown menus behind notifications could not be hovered

commit f4d3cd3dad89d0de84f131dbef7268422b26aa35
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed Aug 29 11:16:41 2018 +0200

    Move navigation to top

commit 564d972291aebb382d1ca560a21fad332d70cd0c
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed Aug 29 10:23:17 2018 +0200

    Move audio player into its own component

commit 382e9e88021db1506efc5fb78935b7846b8257db
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed Aug 29 10:11:14 2018 +0200

    Remove link to last.fm in bio

commit f248c2999ca88eeb82769d7491b1e786ee4a7c9d
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed Aug 29 10:01:10 2018 +0200

    Add links to external services & hero headers to album and artist pages
This commit is contained in:
Arne Schlüter 2018-08-29 11:43:59 +02:00
commit 29ea86479c
17 changed files with 223 additions and 179 deletions

View file

@ -0,0 +1,38 @@
(ns airsonic-ui.components.artist.views
(:require [airsonic-ui.views.album :as album]
[clojure.string :as str]))
(defn link-button [attrs children]
[:p.control>a.button.is-small (merge attrs {:target "_blank"}) children])
(defn lastfm-bio
"Displays the last.fm biography without the 'Read more on Last.fm' link"
[artist-info]
(when (:biography artist-info)
(let [biography (str/replace (:biography artist-info) #"<a .*?>$" "")]
[:p {:dangerouslySetInnerHTML {:__html biography}}])))
(defn lastfm-link [artist-info]
[link-button {:href (:lastFmUrl artist-info)} "See on last.fm"])
(defn musicbrainz-link [artist-info]
(let [href (str "https://musicbrainz.org/artist/" (:musicBrainzId artist-info))]
[link-button {:href href} "See on musicbrainz"]))
(defn detail
"Creates a nice artist page displaying the artist's name, bio (if available and
listing) their albums."
[{:keys [artist artist-info]}]
[:div
[:section.hero>div.hero-body
[:div.container
[:h2.title (:name artist)]
[:div.content
[lastfm-bio artist-info]
(when-not (empty? (select-keys artist-info [:lastFmUrl :musicBrainzId]))
[:div.field.is-grouped
(when (:lastFmUrl artist-info)
[lastfm-link artist-info])
(when (:musicBrainzId artist-info)
[musicbrainz-link artist-info])])]]]
[:section.section>div.container [album/listing (:album artist)]]])

View file

@ -0,0 +1,69 @@
(ns airsonic-ui.components.audio-player.events
(:require [re-frame.core :as re-frame]
[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))
:db (assoc-in db [:audio :playlist] playlist)})))
;; FIXME: :audio/play might not get the right argument here
(re-frame/reg-event-db
:audio-player/set-playback-mode
(fn [db [_ playback-mode]]
(update-in db [:audio :playlist] #(playlist/set-playback-mode % playback-mode))))
(re-frame/reg-event-db
:audio-player/set-repeat-mode
(fn [db [_ repeat-mode]]
(update-in db [:audio :playlist] #(playlist/set-repeat-mode % repeat-mode))))
(re-frame/reg-event-fx
:audio-player/next-song
(fn [{:keys [db]} _]
(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)})))
(re-frame/reg-event-fx
:audio-player/previous-song
(fn [{:keys [db]} _]
(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)})))
(re-frame/reg-event-db
:audio-player/enqueue-next
(fn [db [_ song]]
(update-in db [:audio :playlist] #(playlist/enqueue-next % song))))
(re-frame/reg-event-db
:audio-player/enqueue-last
(fn [db [_ song]]
(update-in db [:audio :playlist] #(playlist/enqueue-last % song))))
(re-frame/reg-event-fx
:audio-player/toggle-play-pause
(fn [_ _]
{:audio/toggle-play-pause nil}))
(defn audio-update
"Reacts to audio events fired by the HTML5 audio player and plays the next
track if necessary."
[{:keys [db]} [_ status]]
(cond-> {:db (assoc-in db [:audio :playback-status] status)}
(:ended? status) (assoc :dispatch [:audio-player/next-song])))
(re-frame/reg-event-fx :audio/update audio-update)

View file

@ -0,0 +1,65 @@
(ns airsonic-ui.components.audio-player.views
(:require [re-frame.core :refer [subscribe]]
[airsonic-ui.helpers :refer [add-classes dispatch]]
[airsonic-ui.views.cover :refer [cover]]
[airsonic-ui.views.icon :refer [icon]]))
;; currently playing / coming next / audio controls...
(defn current-song-info [song status]
[:article
[:div (:artist song) " - " (:title song)]
;; FIXME: Sometimes items don't have a duration
[:progress.progress.is-tiny {:value (:current-time status)
:max (:duration song)}]])
(defn song-controls [is-playing?]
[:div.field.has-addons
(let [buttons [[:media-step-backward :audio-player/previous-song]
[(if is-playing? :media-pause :media-play) :audio-player/toggle-play-pause]
[:media-step-forward :audio-player/next-song]]]
(map (fn [[icon-glyph event]]
^{:key icon-glyph} [:p.control>button.button.is-light
{:on-click (dispatch [event])}
[icon icon-glyph]])
buttons))])
(defn- toggle-shuffle [playback-mode]
(dispatch [:audio-player/set-playback-mode (if (= playback-mode :shuffled)
:linear :shuffled)]))
(defn- toggle-repeat-mode [current-mode]
(let [modes (cycle '(:repeat-none :repeat-all :repeat-single))
next-mode (->> (drop-while (partial not= current-mode) modes)
(second))]
(dispatch [:audio-player/set-repeat-mode next-mode])))
(defn playback-mode-controls [playlist]
(let [{:keys [repeat-mode playback-mode]} playlist
button :p.control>button.button.is-light
shuffle-button (add-classes button (when (= playback-mode :shuffled) :is-primary))
repeat-button (add-classes button (case repeat-mode
:repeat-single :is-info
:repeat-all :is-primary
nil))]
[:div.field.has-addons
^{:key :shuffle-button} [shuffle-button {:on-click (toggle-shuffle playback-mode)} [icon :random]]
^{:key :repeat-button} [repeat-button {:on-click (toggle-repeat-mode repeat-mode)} [icon :loop]]]))
(defn audio-player []
(let [current-song @(subscribe [:audio/current-song])
playlist @(subscribe [:audio/playlist])
playback-status @(subscribe [:audio/playback-status])
is-playing? @(subscribe [:audio/is-playing?])]
[:nav.navbar.is-fixed-bottom.audio-player
[:div.navbar-menu.is-active
(if current-song
;; show song info
[:section.level.audio-interaction
[:div.level-left>article.media
[:div.media-left [cover current-song 48]]
[:div.media-content [current-song-info current-song playback-status]]]
[:div.level-right [song-controls is-playing?]]
[:div.level-right [playback-mode-controls playlist]]]
;; not playing anything
[:p.has-text-light.navbar-item.idle-notification "Select a song to start playing"])]]))

View file

@ -0,0 +1,12 @@
(ns airsonic-ui.components.collection.views
(:require [airsonic-ui.views.song :as song]))
(defn detail
"Lists all songs in an album"
[{:keys [album]}]
[:div
[:section.hero>div.hero-body
[:div.container
[:h2.title (:name album)]
[:h3.subtitle (:artist album)]]]
[:section.section>div.container [song/listing (:song album)]]])

View file

@ -0,0 +1,7 @@
(ns airsonic-ui.components.debug.views
(:require [clojure.pprint :refer [pprint]]))
(defn debug
"Returns a nicely formatted debug view of any given data structure"
[data]
[:pre (with-out-str (pprint data))])

View file

@ -16,7 +16,7 @@
page as its argument. When `max-pages` is `nil` an infinite pagination
will be rendered."
[{:keys [url-fn max-pages current-page]}]
[:nav.pagination.is-centered {:role "pagination", :aria-label "pagination"}
[:nav.pagination {:role "pagination", :aria-label "pagination"}
[:a.pagination-previous (if (> current-page 1)
{:href (url-fn (dec current-page))}
{:disabled true}) "Previous page"]
@ -35,25 +35,26 @@
current-page? (add-classes :is-current))
(cond-> {:href (url-fn page), :aria-label (str "Page " page)}
(= page current-page) (assoc :aria-current "page")) page]))
(when (or (not max-pages) (< max-pages (- max-pages 3)))
(when (or (not max-pages) (< current-page (- max-pages 2)))
^{:key "ellipsis-after"} [:li>span.pagination-ellipsis "…"])]])
(defn main [route {:keys [scan-status album-list]}]
(let [[_ {:keys [criteria]} {:keys [page] :or {page 1}}] route
tab-items [[[::routes/library {:criteria "recent"} nil] "Recently played"]
[[::routes/library {:criteria "newest"} nil] "Newest additions"]
[[::routes/library {:criteria "starred"} nil] "Starred"]]
pagination [pagination {:current-page (int page)
:max-pages 5
:url-fn #(url-for ::routes/library {:criteria criteria} {:page %})}]]
[:div
[:h2.title "Your library"]
(if (:count scan-status)
[:p.subtitle.is-5.has-text-grey "Containing " [:strong (:count scan-status)] " items"]
(when (:scanning scan-status)
[:p.subtitle.is-5.has-text-grey "Scanning…"]))
(let [items [[[::routes/library {:criteria "recent"} nil] "Recently played"]
[[::routes/library {:criteria "newest"} nil] "Newest additions"]
[[::routes/library {:criteria "starred"} nil] "Starred"]]]
[tabs {:items items :active-item {:criteria criteria}}])
pagination
[:section.section
[album/listing (:album album-list)]]
pagination]))
[:section.hero.is-small>div.hero-body>div.container
[:h2.title "Your library"]
(if (:count scan-status)
[:p.subtitle.is-5.has-text-grey "Containing " [:strong (:count scan-status)] " items"]
(when (:scanning scan-status)
[:p.subtitle.is-5.has-text-grey "Scanning…"]))]
[:section.section>div.container
[tabs {:items tab-items :active-item {:criteria criteria}}]
pagination
[:section.section [album/listing (:album album-list)]]
pagination]]))

View file

@ -1,6 +1,5 @@
(ns airsonic-ui.components.search.views
(:require [clojure.pprint :refer [pprint]]
[re-frame.core :refer [dispatch subscribe]]
(:require [re-frame.core :refer [dispatch subscribe]]
[goog.functions :refer [debounce]]
[airsonic-ui.routes :as routes :refer [url-for]]
[airsonic-ui.views.song :as song]
@ -47,7 +46,7 @@
(defn results [{:keys [search]}]
(let [term @(subscribe [:search/current-term])]
[:div
[:section.section>div.container
[:h2.title (str "Search results for \"" term "\"")]
(if (empty? search)
[:p "The server returned no results."]
@ -63,5 +62,4 @@
(when-not (empty? (:song search))
[:section.section.is-small
[:h3.subtitle.is-5 "Songs"]
[song-results search]])])
[:pre (with-out-str (pprint search))]]))
[song-results search]])])]))