mirror of
https://github.com/heyarne/airsonic-ui.git
synced 2026-05-06 10:23:39 +02:00
* First sloppy import of code from heyarne/reagent-movable * Consistently use "current queue" to avoid confusion * Update shadow-cljs, re-frame and debux * Solve styling problem when sorting table rows * Make sortable component more reusable * Refactor playlist to use a sorted-map * Make sure current queue is displayed again * Fix sorting when converting a shuffled into a linear playlist * Implement set-current-track * Implement song-move in playlist * Add autoprefixer * Implement drag and drop reordering in current queue * Fix broken dev sass build * Bump some dependencies * Move airsonic-ui.views.icon to bulma.icon * Implement reusable dropdown in bulma.dropdown * Immediately render reordered tracks, reimplement actions in album view * Use new song-table on search result page * Make song-table more reusable * Remove current song * Implement go to source in current queue * Remove unused song view
157 lines
7 KiB
Clojure
157 lines
7 KiB
Clojure
(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]]
|
|
[bulma.icon :refer [icon]]))
|
|
|
|
;; currently playing / coming next / audio controls...
|
|
|
|
(defn seek
|
|
"Calculates the position of the click and sets current playback accordingly"
|
|
[ev]
|
|
(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 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)
|
|
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 playback-controls [is-playing?]
|
|
[: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]
|
|
[: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]
|
|
(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))]
|
|
(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 [{:keys [repeat-mode playback-mode]}]
|
|
(let [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-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.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]]
|
|
^{: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])
|
|
current-playlist @(subscribe [:audio/current-playlist])
|
|
playback-status @(subscribe [:audio/playback-status])
|
|
is-playing? @(subscribe [:audio/is-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?]
|
|
[volume-controls playback-status]
|
|
[playback-mode-controls current-playlist]]
|
|
;; not playing anything
|
|
[:p.navbar-item.idle-notification "No audio playing"])]))
|