mirror of
https://github.com/heyarne/airsonic-ui.git
synced 2026-05-07 10:43:39 +02:00
Mobile improvements (#42)
* Implement variadic url parameters * Trying to make the audio player more mobile friendly All good but progress bar is missing * Implement progress bar with html elements, fixes #39 * Add duration text next to progress bar * Simplify audio player structure * Make albums more comfortably browsable on mobile * Implement responsive generated covers with SVG * Restrict progress bar to 100% max-width * Make search results somewhat usable on mobile * Implement progress bar with svg
This commit is contained in:
parent
d74ef2d41a
commit
a75cdca9e1
10 changed files with 281 additions and 232 deletions
|
|
@ -1,114 +1,88 @@
|
|||
(ns airsonic-ui.components.audio-player.views
|
||||
(:require [re-frame.core :refer [subscribe dispatch]]
|
||||
[airsonic-ui.routes :as routes]
|
||||
[airsonic-ui.components.highres-canvas.views :refer [canvas]]
|
||||
[airsonic-ui.helpers :refer [add-classes muted-dispatch]]
|
||||
[airsonic-ui.helpers :as h]
|
||||
[airsonic-ui.views.cover :refer [cover]]
|
||||
[airsonic-ui.views.icon :refer [icon]]))
|
||||
|
||||
;; currently playing / coming next / audio controls...
|
||||
;; FIXME: Sometimes items don't have a duration
|
||||
|
||||
(def progress-bar-color "rgb(93,93,93)")
|
||||
(def progress-bar-color-buffered "rgb(143,143,143)")
|
||||
(def progress-bar-color-active "whitesmoke")
|
||||
|
||||
(defn draw-progress [ctx current-time buffered duration]
|
||||
(let [width (.. ctx -canvas -clientWidth)
|
||||
height (.. ctx -canvas -clientHeight)
|
||||
padding 5
|
||||
buffered-x (+ padding (* (- width (* 2 padding)) (min 1 (/ buffered duration))))
|
||||
current-x (+ padding (* (- width (* 2 padding)) (min 1 (/ current-time duration))))]
|
||||
;; vertically center everything
|
||||
(.translate ctx 0.5 (+ (Math/ceil (/ height 2)) 0.5))
|
||||
;; draw complete bar
|
||||
(set! (.-strokeStyle ctx) progress-bar-color)
|
||||
(doto ctx
|
||||
(.beginPath)
|
||||
(.moveTo padding 0)
|
||||
(.lineTo (- width (* 2 padding)) 0)
|
||||
(.stroke))
|
||||
;; draw the buffered part
|
||||
(set! (.-strokeStyle ctx) progress-bar-color-buffered)
|
||||
(doto ctx
|
||||
(.beginPath)
|
||||
(.moveTo padding 0)
|
||||
(.lineTo buffered-x 0)
|
||||
(.stroke))
|
||||
;; draw the part that's already played
|
||||
(set! (.-strokeStyle ctx) progress-bar-color-active)
|
||||
(doto ctx
|
||||
(.beginPath)
|
||||
(.moveTo padding 0)
|
||||
(.lineTo current-x 0)
|
||||
(.stroke))
|
||||
;; draw a dot marking the current time
|
||||
(set! (.-fillStyle ctx) progress-bar-color-active)
|
||||
(doto ctx
|
||||
(.beginPath)
|
||||
(.arc current-x 0 (/ padding 2) 0 (* Math/PI 2))
|
||||
(.fill))))
|
||||
|
||||
(defn current-progress [current-time buffered duration]
|
||||
[canvas {:class "current-progress-canvas"
|
||||
:draw #(draw-progress % current-time buffered duration)}])
|
||||
|
||||
;; FIXME: It's ugly to have the canvas padding and styling scattered everywhere (sass, drawing code above, and here)
|
||||
|
||||
(defn seek
|
||||
"Calculates the position of the click and sets current playback accordingly"
|
||||
[ev]
|
||||
(let [x (- (.. ev -nativeEvent -pageX)
|
||||
(.. ev -target getBoundingClientRect -left))
|
||||
width (- (.. ev -target -nextElementSibling -clientWidth) 10)] ;; <- 10 = 2 * canvas-padding
|
||||
(dispatch [:audio-player/seek (/ x width)])))
|
||||
(let [x-ratio (/ (.. ev -nativeEvent -layerX)
|
||||
(.. ev -target -parentElement getBoundingClientRect -width))]
|
||||
(dispatch [:audio-player/seek x-ratio])))
|
||||
|
||||
(defn buffered-part
|
||||
[buffered duration]
|
||||
(let [width (min 100 (* (/ buffered duration) 100))]
|
||||
[:div.buffered-part {:on-click seek
|
||||
:style {:width (str "calc(" width "% - 1rem - 10px)")}}]))
|
||||
(defn- ratio->width [ratio]
|
||||
(str (.toFixed (min 100 (* 100 ratio)) 2) "%"))
|
||||
|
||||
(defn current-song-info [song status]
|
||||
(defn progress-bars [buffered-width played-width]
|
||||
[:svg.progress-bars {:aria-hidden "true"}
|
||||
[:svg.complete-song-bar
|
||||
[:rect {:x 0, :y "50%", :width "100%", :height 1}]]
|
||||
[:svg.buffered-part-bar
|
||||
[:rect.click-dummy {:on-click seek
|
||||
:x 0, :y 0, :width buffered-width, :height "100%"}]
|
||||
[:rect {:x 0, :y "50%", :width buffered-width, :height 1}]]
|
||||
[:svg.played-back-bar
|
||||
[:rect {:x 0, :y "50%", :width played-width, :height 1}]
|
||||
[:circle {:cx played-width, :cy "50%", :r 2.5}]]])
|
||||
|
||||
(defn progress-indicators [song status]
|
||||
(let [current-time (:current-time status)
|
||||
buffered (:buffered status)
|
||||
duration (:duration song)]
|
||||
[:article.current-song-info
|
||||
[:div.current-name (:artist song) [:br] (:title song)]
|
||||
[:div.current-progress
|
||||
[buffered-part buffered duration]
|
||||
[current-progress current-time buffered duration]]]))
|
||||
duration (:duration song)
|
||||
progress-text (str (h/format-duration current-time :brief? true)
|
||||
" / "
|
||||
(h/format-duration duration :brief? true))
|
||||
buffered-width (ratio->width (/ buffered duration))
|
||||
played-width (ratio->width (/ current-time duration))]
|
||||
[:article.progress-indicators
|
||||
[progress-bars buffered-width played-width]
|
||||
[:div.progress-info-text.duration-text progress-text]]))
|
||||
|
||||
(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]]
|
||||
title {:media-step-backward "Previous"
|
||||
:media-play "Play"
|
||||
:media-pause "Pause"
|
||||
:media-step-forward "Next"}]
|
||||
(for [[icon-glyph event] buttons]
|
||||
^{:key icon-glyph} [:p.control [:button.button.is-light
|
||||
{:on-click (muted-dispatch [event])
|
||||
:title (title icon-glyph)}
|
||||
[icon icon-glyph]]]))])
|
||||
(defn playback-info [song status]
|
||||
[:a.playback-info.media
|
||||
{:href (routes/url-for ::routes/current-queue)
|
||||
:title "Go to current queue"}
|
||||
[:div.media-left [cover song 64]]
|
||||
[:div.media-content
|
||||
[:div.artist-and-title
|
||||
[:span.artist(:artist song)]
|
||||
[:span.song-title (:title song)]]]])
|
||||
|
||||
(defn playback-controls [is-playing?]
|
||||
[:div.playback-controls
|
||||
[: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]]
|
||||
title {:media-step-backward "Previous"
|
||||
:media-play "Play"
|
||||
:media-pause "Pause"
|
||||
:media-step-forward "Next"}]
|
||||
(for [[icon-glyph event] buttons]
|
||||
^{:key icon-glyph} [:p.control [:button.button.is-light
|
||||
{:on-click (h/muted-dispatch [event])
|
||||
:title (title icon-glyph)}
|
||||
[icon icon-glyph]]]))]])
|
||||
|
||||
(defn- toggle-shuffle [playback-mode]
|
||||
(muted-dispatch [:audio-player/set-playback-mode (if (= playback-mode :shuffled)
|
||||
(h/muted-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))]
|
||||
(muted-dispatch [:audio-player/set-repeat-mode next-mode])))
|
||||
(h/muted-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
|
||||
shuffle-button (h/add-classes button (when (= playback-mode :shuffled) :is-primary))
|
||||
repeat-button (h/add-classes button (case repeat-mode
|
||||
:repeat-single :is-info
|
||||
:repeat-all :is-primary
|
||||
nil))
|
||||
|
|
@ -116,28 +90,25 @@
|
|||
:repeat-all "Repeating current queue, click to repeat current track"
|
||||
:repeat-single "Repeating current track, click to repeat none"
|
||||
"Click to repeat current queue")]
|
||||
[:div.field.has-addons
|
||||
^{:key :shuffle-button} [shuffle-button {:on-click (toggle-shuffle playback-mode)
|
||||
:title "Shuffle"} [icon :random]]
|
||||
^{:key :repeat-button} [repeat-button {:on-click (toggle-repeat-mode repeat-mode)
|
||||
:title repeat-title} [icon :loop]]]))
|
||||
[:div.playback-mode-controls
|
||||
[:div.button-group>div.field.has-addons
|
||||
^{:key :shuffle-button} [shuffle-button {:on-click (toggle-shuffle playback-mode)
|
||||
:title "Shuffle"} [icon :random]]
|
||||
^{:key :repeat-button} [repeat-button {:on-click (toggle-repeat-mode repeat-mode)
|
||||
:title repeat-title} [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
|
||||
[:div.button-group [:p.control>a.button.is-light {:href (routes/url-for ::routes/current-queue) :title "Go to current queue"} [icon :menu]]]
|
||||
[:div.button-group [song-controls is-playing?]]
|
||||
[:div.button-group [playback-mode-controls playlist]]]]
|
||||
;; not playing anything
|
||||
[:p.navbar-item.idle-notification "No audio playing"])]]))
|
||||
[:nav.audio-player
|
||||
(if current-song
|
||||
;; show song info, controls, progress bar, etc.
|
||||
[:section.audio-interaction
|
||||
[playback-info current-song playback-status]
|
||||
[progress-indicators current-song playback-status]
|
||||
[playback-controls is-playing?]
|
||||
[playback-mode-controls playlist]]
|
||||
;; not playing anything
|
||||
[:p.navbar-item.idle-notification "No audio playing"])]))
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
[:div
|
||||
[:section.hero.is-small>div.hero-body
|
||||
[:div.container
|
||||
[:article.media
|
||||
[:article.collection-header.media
|
||||
[:div.media-left [cover album 128]]
|
||||
[:div.media-content
|
||||
[:h2.title (:name album)]
|
||||
|
|
|
|||
|
|
@ -19,28 +19,27 @@
|
|||
:default-value search-term
|
||||
:placeholder "Search"}]]])))
|
||||
|
||||
(defn result-cards [items]
|
||||
[:div.columns.is-multiline.is-mobile
|
||||
(for [[url item] items]
|
||||
^{:key url} [:div.column.is-one-fifth-tablet.is-one-third-mobile
|
||||
[card item
|
||||
:url-fn (constantly url)
|
||||
:content [:div>a
|
||||
{:href url, :title (:name item)}
|
||||
(:name item)]]])])
|
||||
|
||||
(defn- artist-url [artist]
|
||||
(url-for ::routes/artist.detail (select-keys artist [:id])))
|
||||
|
||||
(defn artist-results [{:keys [artist]}]
|
||||
[:div.columns.is-multiline.is-mobile
|
||||
(for [[idx artist] (map-indexed vector artist)]
|
||||
(let [url #(url-for ::routes/artist.detail (select-keys % [:id]))]
|
||||
^{:key idx} [:div.column.is-2
|
||||
[card artist
|
||||
:url-fn url
|
||||
:content [:div>a
|
||||
{:href (url artist), :title (:name artist)}
|
||||
(:name artist)]]]))])
|
||||
[result-cards (map (juxt artist-url identity) artist)])
|
||||
|
||||
(defn- album-url [album]
|
||||
(url-for ::routes/album.detail (select-keys album [:id])))
|
||||
|
||||
(defn album-results [{:keys [album]}]
|
||||
[:div.columns.is-multiline.is-mobile
|
||||
(for [[idx album] (map-indexed vector album)]
|
||||
(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
|
||||
:content [:div>a
|
||||
{:href (url album), :title title}
|
||||
title]]]))])
|
||||
[result-cards (map (juxt album-url identity) album)])
|
||||
|
||||
(defn song-results [{:keys [song]}]
|
||||
[song/listing song])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue