-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
10 changed files
with
281 additions
and
232 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
175 changes: 73 additions & 102 deletions
175
src/cljs/airsonic_ui/components/audio_player/views.cljs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"])])) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.