Skip to content

Commit

Permalink
Mobile improvements (#42)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rrrnld authored Jan 23, 2019
1 parent d74ef2d commit a75cdca
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 232 deletions.
28 changes: 14 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 15 additions & 3 deletions src/cljs/airsonic_ui/api/helpers.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,29 @@
:c "airsonic-ui-cljs"
:v "1.15.0"})

(defn- unroll-variadic-params
"Turns {:id [1 2 3], :foo :bar} into [[:id 1] [:id 2] [:id 3] [:foo :bar]]"
[params]
(->>
(map (fn [[k vs]]
(if (sequential? vs)
(map (fn [v] [k v]) vs)
[k vs])) params)
(flatten)
(partition 2)))

(def ^:private encode js/encodeURIComponent)

(defn url
"Returns an absolute url to an API endpoint"
[credentials endpoint params]
(let [server (:server credentials)
query (->> (merge default-params (select-keys credentials [:u :p]) params)
auth (select-keys credentials [:u :p])
query (->> (merge default-params auth params)
(unroll-variadic-params)
(map (fn [[k v]] (str (encode (name k)) "=" (encode v))))
(str/join "&"))]
(str server (when-not (str/ends-with? server "/") "/") "rest/" endpoint "?" query)))
(str (str/replace server #"/+$" "") "/rest/" endpoint "?" query)))

(defn stream-url [credentials song-or-episode]
;; podcasts have a stream-id, normal songs just use their id
Expand Down Expand Up @@ -63,4 +76,3 @@
#{:artistId :name :songCount :artist} :album
#{:id :name :albumCount} :artist
:unknown)))

175 changes: 73 additions & 102 deletions src/cljs/airsonic_ui/components/audio_player/views.cljs
Original file line number Diff line number Diff line change
@@ -1,143 +1,114 @@
(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- ratio->width [ratio]
(str (.toFixed (min 100 (* 100 ratio)) 2) "%"))

(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 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 current-song-info [song status]
(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 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 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-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))
repeat-title (case repeat-mode
: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"])]))
2 changes: 1 addition & 1 deletion src/cljs/airsonic_ui/components/collection/views.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Loading

0 comments on commit a75cdca

Please sign in to comment.