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

Implement custom progress indicator and seeking

Squashed commit of the following:

commit 23b9a3deac564bf3753a00238784a6045cb50d46
Author: Arne Schlüter <arne@schlueter.is>
Date:   Sun Oct 14 10:20:08 2018 +0200

    Enable seeking in buffered part and fix drawn x value

commit 9ce4b0941f4a57286f608d2b155658672cac3817
Author: Arne Schlüter <arne@schlueter.is>
Date:   Sun Oct 14 09:40:43 2018 +0200

    Draw seek position and enable seeking played part by click

commit 58cbf2d8035c0eeacaed3da7a68f97d94db4a2b6
Author: Arne Schlüter <arne@schlueter.is>
Date:   Thu Oct 11 21:42:57 2018 +0200

    Add retina canvas

commit 6acb84a67e4bee61e5b9ae6eb15e8159e0431662
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed Oct 10 17:52:43 2018 +0200

    Implement canvas progress bar
This commit is contained in:
Arne Schlüter 2018-10-14 10:31:47 +02:00
commit 513169ea71
9 changed files with 211 additions and 74 deletions

View file

@ -61,3 +61,9 @@
(:ended? status) (assoc :dispatch [:audio-player/next-song])))
(re-frame/reg-event-fx :audio/update audio-update)
(re-frame/reg-event-fx
:audio-player/seek
(fn [{:keys [db]} [_ percentage]]
(let [duration (:duration (playlist/peek (get-in db [:audio :playlist])))]
{:audio/seek [percentage duration]})))

View file

@ -1,18 +1,80 @@
(ns airsonic-ui.components.audio-player.views
(:require [re-frame.core :refer [subscribe]]
(: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.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(123,123,123)")
(def progress-bar-color-active "whitesmoke")
(defn draw-progress [ctx current-time seekable duration]
(let [width (.. ctx -canvas -clientWidth)
height (.. ctx -canvas -clientHeight)
padding 5
seekable-x (+ padding (* (- width (* 2 padding)) (min 1 (/ seekable 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 seekable-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 seekable duration]
[canvas {:class-name "current-progress-canvas"
:draw draw-progress} current-time seekable duration])
(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)]
(dispatch [:audio-player/seek (/ x width)])))
(defn buffered-part
[seekable duration]
[:div.buffered-part {:on-click seek
:style {:width (str (min 100 (* (/ seekable duration) 100)) "%")}}])
(defn current-song-info [song status]
[:article.current-song-info
[:span (:artist song) " - " (:title song)]
;; FIXME: Sometimes items don't have a duration
[:progress.progress.is-tiny {:value (:current-time status)
:max (:duration song)}]])
(let [current-time (:current-time status)
seekable (:seekable status)
duration (:duration song)]
[:article.current-song-info
[:div.current-name (:artist song) [:br] (:title song)]
[:div.current-progress
[buffered-part seekable duration]
[current-progress current-time seekable duration]]]))
(defn song-controls [is-playing?]
[:div.field.has-addons
@ -25,14 +87,14 @@
:media-step-forward "Next"}]
(map (fn [[icon-glyph event]]
^{:key icon-glyph} [:p.control>button.button.is-light
{:on-click (muted-dispatch [event])
:title (title icon-glyph)}
[icon icon-glyph]])
{:on-click (muted-dispatch [event])
:title (title icon-glyph)}
[icon icon-glyph]])
buttons))])
(defn- toggle-shuffle [playback-mode]
(muted-dispatch [:audio-player/set-playback-mode (if (= playback-mode :shuffled)
:linear :shuffled)]))
:linear :shuffled)]))
(defn- toggle-repeat-mode [current-mode]
(let [modes (cycle '(:repeat-none :repeat-all :repeat-single))

View file

@ -1,20 +1,12 @@
(ns airsonic-ui.components.collection.views
"A collection is a list of audio files that belong together (e.g. an album or
a podcast's overview)"
(:require [airsonic-ui.routes :as routes :refer [url-for]]
(:require [airsonic-ui.helpers :refer [format-duration]]
[airsonic-ui.routes :as routes :refer [url-for]]
[airsonic-ui.views.cover :refer [cover card]]
[airsonic-ui.views.icon :refer [icon]]
[airsonic-ui.views.song :as song]))
(defn format-duration [seconds]
(let [hours (quot seconds 3600)
minutes (quot (rem seconds 3600) 60)
seconds (rem seconds 60)]
(-> (cond-> ""
(> hours 0) (str hours "h ")
(> minutes 0) (str minutes "m "))
(str seconds "s"))))
(defn collection-info [{:keys [songCount duration year]}]
(vec (cond-> [:ul.is-smaller.collection-info
[:li [icon :audio-spectrum] (str songCount (if (= 1 songCount)
@ -22,7 +14,6 @@
[:li [icon :clock] (format-duration duration)]]
year (conj [:li [icon :calendar] (str "Released in " year)]))))
(defn album-card [album]
(let [{:keys [artist artistId name id]} album]
[card album

View file

@ -0,0 +1,33 @@
(ns airsonic-ui.components.highres-canvas.views
"This module provides a reusable canvas component. You can provide a drawing
function via the `:draw` attribute which will be passed a 2d rendering
context. It will automatically be drawn in high resolution on retina displays."
(:require [reagent.core :as reagent]))
(defn redraw [this]
(let [[_ {draw :draw} & props] (reagent/argv this)
canvas (reagent/dom-node this)
width (.-clientWidth canvas)
height (.-clientHeight canvas)
ctx (.getContext canvas "2d")
pixel-ratio (.-devicePixelRatio js/window)]
(set! (. canvas -width) width)
(set! (. canvas -height) height)
;; retina drawing code:
;; set up dimensions, reset the transform matrix to the identity
;; matrix and automatically scale up
(when (> pixel-ratio 1)
(set! (. canvas -width) (* pixel-ratio width))
(set! (. canvas -height) (* pixel-ratio height))
(set! (.. canvas -style -width) (str width "px"))
(set! (.. canvas -style -height) (str height "px"))
(.setTransform ctx 1 0 0 1 0 0)
(.scale ctx pixel-ratio pixel-ratio))
(apply draw ctx props)))
(defn canvas [attrs & _]
(reagent/create-class
{:component-did-update redraw
:component-did-mount redraw
:render (fn render []
[:canvas.highres-canvas (dissoc attrs :draw)])}))