From 53748941c0d1b5439ff5774fe9e872c5ded06d3d Mon Sep 17 00:00:00 2001 From: heyarne Date: Sat, 2 Mar 2019 10:28:13 +0100 Subject: [PATCH] Volume Controls (#46) * Implement always-visible volume controls * Implement toggling of volume controls * Change icon based on volume level * Add volume control keyboard shortcuts * Check left mouse button for all events fired when changing the volume --- src/cljs/airsonic_ui/audio/core.cljs | 34 +++++++++-- .../components/audio_player/events.cljs | 15 +++++ .../components/audio_player/views.cljs | 54 +++++++++++++++-- .../components/keyboard_shortcuts/config.cljs | 6 ++ src/sass/app.sass | 58 +++++++++++++++++-- 5 files changed, 152 insertions(+), 15 deletions(-) diff --git a/src/cljs/airsonic_ui/audio/core.cljs b/src/cljs/airsonic_ui/audio/core.cljs index d0a2f42..bd52044 100644 --- a/src/cljs/airsonic_ui/audio/core.cljs +++ b/src/cljs/airsonic_ui/audio/core.cljs @@ -23,14 +23,15 @@ :current-src (.-currentSrc elem) :current-time (.-currentTime elem) :seekable (normalize-time-ranges (.-seekable elem)) - :buffered (normalize-time-ranges (.-buffered elem))}) + :buffered (normalize-time-ranges (.-buffered elem)) + :volume (.-volume elem)}) ; explanation of these events: https://developer.mozilla.org/en-US/Apps/Fundamentals/Audio_and_video_delivery/Cross-browser_audio_basics (defn attach-listeners! [el] (let [emit-audio-update (throttle #(rf/dispatch [:audio/update (->status el)]) 16)] - (doseq [event ["loadstart" "progress" "play" "timeupdate" "pause"]] + (doseq [event ["loadstart" "progress" "play" "timeupdate" "pause" "volumechange"]] (.addEventListener el event emit-audio-update)))) ;; effects to be fired from event handlers @@ -71,6 +72,27 @@ (set! (. @audio -currentTime) (* percentage duration)))) +(defn- set-volume! [volume] + (set! (.-volume @audio) volume)) + +(rf/reg-fx + :audio/set-volume + (fn [percentage] + (when @audio + (set-volume! percentage)))) + +(rf/reg-fx + :audio/increase-volume + (fn [_] + (when-let [vol (some-> @audio .-volume)] + (set-volume! (min 1 (+ vol 0.05)))))) + +(rf/reg-fx + :audio/decrease-volume + (fn [_] + (when-let [vol (some-> @audio .-volume)] + (set-volume! (max 0 (- vol 0.05)))))) + ;; subscriptions (defn summary @@ -87,7 +109,7 @@ (rf/reg-sub :audio/playlist - (fn [_ _] (rf/subscribe [:audio/summary])) + :<- [:audio/summary] playlist) (defn current-song @@ -98,7 +120,7 @@ (rf/reg-sub :audio/current-song - (fn [_ _] (rf/subscribe [:audio/playlist])) + :<- [:audio/playlist] current-song) (defn playback-status @@ -108,7 +130,7 @@ (rf/reg-sub :audio/playback-status - (fn [_ _] (rf/subscribe [:audio/summary])) + :<- [:audio/summary] playback-status) (defn is-playing? @@ -119,5 +141,5 @@ (rf/reg-sub :audio/is-playing? - (fn [_ _] (rf/subscribe [:audio/playback-status])) + :<- [:audio/playback-status] is-playing?) diff --git a/src/cljs/airsonic_ui/components/audio_player/events.cljs b/src/cljs/airsonic_ui/components/audio_player/events.cljs index 615dcbb..de96947 100644 --- a/src/cljs/airsonic_ui/components/audio_player/events.cljs +++ b/src/cljs/airsonic_ui/components/audio_player/events.cljs @@ -67,3 +67,18 @@ (fn [{:keys [db]} [_ percentage]] (let [duration (:duration (playlist/peek (get-in db [:audio :playlist])))] {:audio/seek [percentage duration]}))) + +(rf/reg-event-fx + :audio-player/set-volume + (fn [_ [_ percentage]] + {:audio/set-volume percentage})) + +(rf/reg-event-fx + :audio-player/increase-volume + (fn [_ _] + {:audio/increase-volume nil})) + +(rf/reg-event-fx + :audio-player/decrease-volume + (fn [_ _] + {:audio/decrease-volume nil})) diff --git a/src/cljs/airsonic_ui/components/audio_player/views.cljs b/src/cljs/airsonic_ui/components/audio_player/views.cljs index 4959634..e1dfacf 100644 --- a/src/cljs/airsonic_ui/components/audio_player/views.cljs +++ b/src/cljs/airsonic_ui/components/audio_player/views.cljs @@ -1,5 +1,6 @@ (ns airsonic-ui.components.audio-player.views (:require [re-frame.core :refer [subscribe dispatch]] + [reagent.core :as r] [airsonic-ui.routes :as routes] [airsonic-ui.helpers :as h] [airsonic-ui.views.cover :refer [cover]] @@ -53,7 +54,7 @@ [:span.song-title (:title song)]]]]) (defn playback-controls [is-playing?] - [:div.playback-controls + [:div.button-controls.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] @@ -78,19 +79,61 @@ (second))] (h/muted-dispatch [:audio-player/set-repeat-mode next-mode]))) +(defn set-volume [ev] + (when (= 1 (.-buttons ev)) ;; only on left-click + (let [y-ratio (/ (.. ev -nativeEvent -offsetY) + (.. ev -target getBoundingClientRect -height))] + (dispatch [:audio-player/set-volume (- 1 y-ratio)])))) + +(defonce volume-slider-visible? (r/atom false)) + +(defn volume-slider [volume] + (let [y-pos (* (- 1 volume) 100)] + [:svg.volume-bar {:width "100%", :height "100%"} + ;; the translate(...) makes the 1px rects look smoother + [:g {:transform "translate(-0.5,0)"} + ;; background line + [:rect.inactive {:x "50%", :y 0, :width 1, :height "100%"}] + ;; below are the line and circle that show the current volume + [:rect.active {:x "50%", :y (str y-pos "%"), + :width 1, :height (str (- 100 y-pos) "%")}]] + [:circle.active {:cx "50%", :cy (str y-pos "%"), :r 3}] + [:rect.click-dummy {:x 0, :y 0, :width "100%", :height "100%" + :on-mouse-down set-volume + :on-mouse-up set-volume + :on-mouse-move set-volume}]])) + +(def toggle-volume-slider #(swap! volume-slider-visible? not)) +(def hide-volume-slider #(reset! volume-slider-visible? false)) + +(defn volume-controls [playback-status] + (let [volume (:volume playback-status) + volume-icon (cond + (> volume 0.66) :volume-high + (> volume 0.1) :volume-low + :else :volume-off)] + [:div.button-controls.volume-controls + (when @volume-slider-visible? + [:div.button-menu + [:div.button-menu-closer {:on-click hide-volume-slider}] + [volume-slider volume]]) + [:p.control>button.button.is-light + {:on-click toggle-volume-slider} + [icon volume-icon]]])) + (defn playback-mode-controls [playlist] (let [{:keys [repeat-mode playback-mode]} playlist button :p.control>button.button.is-light 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-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.playback-mode-controls + [:div.button-controls.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]] @@ -109,6 +152,7 @@ [playback-info current-song playback-status] [progress-indicators current-song playback-status] [playback-controls is-playing?] + [volume-controls playback-status] [playback-mode-controls playlist]] ;; not playing anything [:p.navbar-item.idle-notification "No audio playing"])])) diff --git a/src/cljs/airsonic_ui/components/keyboard_shortcuts/config.cljs b/src/cljs/airsonic_ui/components/keyboard_shortcuts/config.cljs index 3e51c29..5204106 100644 --- a/src/cljs/airsonic_ui/components/keyboard_shortcuts/config.cljs +++ b/src/cljs/airsonic_ui/components/keyboard_shortcuts/config.cljs @@ -14,6 +14,12 @@ ["→" "Next song" [:audio-player/next-song] [{:keyCode 39}]] + ["+" "Increase volume" + [:audio-player/increase-volume] + [{:keyCode 171}]] + ["-" "Decrease volume" + [:audio-player/decrease-volume] + [{:keyCode 173}]] ["?" "Show / hide keyboard shortcut help" [:bulma.modal.events/toggle :keyboard-shortcuts-help] [{:keyCode 63}]]]) diff --git a/src/sass/app.sass b/src/sass/app.sass index 826ce7e..3df4f2c 100644 --- a/src/sass/app.sass +++ b/src/sass/app.sass @@ -122,13 +122,63 @@ fill: $dark-invert // buttons to control current playback and playlist behavior - .playback-controls, - .playback-mode-controls + .button-controls + position: relative flex-shrink: 0 padding-right: .6rem - .playback-controls - padding-left: .6rem + &:first-of-type + padding-left: .6rem + + .button-menu + svg.volume-bar + overflow: visible + + .inactive + fill: $background + + .active + fill: $link + + .click-dummy + cursor: pointer + fill: transparent + +.button-menu-closer + // this element is needed so we can have a "click-outside" + position: fixed + z-index: -1 + top: 0 + left: 0 + right: 0 + bottom: 0 + +.button-menu + position: absolute + z-index: 100 + width: 36px + bottom: calc(100% + .3em) + padding: $button-padding-horizontal $button-padding-horizontal / 2 + + border-radius: $radius + background: $white + color: $dark + box-shadow: 0 0 2px rgba(0,0,0,.1), 0 0 4px rgba(0,0,0,.1) + + // little arrow at the bottom + &::after + position: absolute + content: '' + display: block + width: 6px + height: 6px + background: inherit + top: 100% + left: 50% + margin-left: -3px + margin-top: -3px + transform: rotate(45deg) + box-shadow: 2px 2px 1px rgba(0,0,0,.1) // preview card for album or artist listings .preview-card