Browse Source

feat: add buffered progress bar to bg player and refine styles

Miguel Ángel Moreno 3 months ago
parent
commit
6edf25f948

+ 5 - 0
src/frontend/tubo/bg_player/events.cljs

@@ -21,6 +21,11 @@
    {:player/pause {:paused? (not paused?)
                    :player  bg-player}}))
 
+(rf/reg-event-db
+ :bg-player/set-loading
+ (fn [db [_ val]]
+   (assoc db :bg-player/loading val)))
+
 (rf/reg-event-fx
  :bg-player/start
  [(rf/inject-cofx ::inject/sub [:bg-player])

+ 9 - 1
src/frontend/tubo/bg_player/subs.cljs

@@ -1,6 +1,7 @@
 (ns tubo.bg-player.subs
   (:require
-   [re-frame.core :as rf]))
+   [re-frame.core :as rf]
+   [reagent.core :as r]))
 
 (defonce !player (atom nil))
 
@@ -23,3 +24,10 @@
  :bg-player
  (fn []
    !player))
+
+(defonce !buffered (r/atom 0))
+
+(rf/reg-sub
+ :bg-player/buffered
+ (fn []
+   !buffered))

+ 86 - 90
src/frontend/tubo/bg_player/views.cljs

@@ -1,6 +1,5 @@
 (ns tubo.bg-player.views
   (:require
-   [clojure.string :as str]
    [re-frame.core :as rf]
    [reagent.dom :as rdom]
    [reagent.core :as r]
@@ -9,54 +8,6 @@
    [tubo.layout.views :as layout]
    [tubo.utils :as utils]))
 
-(defonce base-slider-classes
-  ["h-2" "cursor-pointer" "appearance-none" "bg-neutral-300"
-   "dark:bg-neutral-600"
-   "rounded-full" "overflow-hidden" "focus:outline-none"
-   "[&::-webkit-slider-thumb]:appearance-none"
-   "[&::-webkit-slider-thumb]:border-0"
-   "[&::-webkit-slider-thumb]:rounded-full"
-   "[&::-webkit-slider-thumb]:h-2"
-   "[&::-webkit-slider-thumb]:w-2"
-   "[&::-webkit-slider-thumb]:shadow-[-405px_0_0_400px]"
-   "[&::-moz-range-thumb]:border-0"
-   "[&::-moz-range-thumb]:rounded-full"
-   "[&::-moz-range-thumb]:h-2"
-   "[&::-moz-range-thumb]:w-2"
-   "[&::-moz-range-thumb]:shadow-[-405px_0_0_400px]"])
-
-(defn get-slider-shadow-classes
-  [service-color]
-  (case service-color
-    "#cc0000" ["[&::-webkit-slider-thumb]:shadow-[#cc0000]"
-               "[&::-moz-range-thumb]:shadow-[#cc0000]"]
-    "#ff7700" ["[&::-webkit-slider-thumb]:shadow-[#ff7700]"
-               "[&::-moz-range-thumb]:shadow-[#ff7700]"]
-    "#333333" ["[&::-webkit-slider-thumb]:shadow-[#333333]"
-               "[&::-moz-range-thumb]:shadow-[#333333]"]
-    "#F2690D" ["[&::-webkit-slider-thumb]:shadow-[#F2690D]"
-               "[&::-moz-range-thumb]:shadow-[#F2690D]"]
-    "#629aa9" ["[&::-webkit-slider-thumb]:shadow-[#629aa9]"
-               "[&::-moz-range-thumb]:shadow-[#629aa9]"]
-    ["[&::-webkit-slider-thumb]:shadow-neutral-300"
-     "[&::-moz-range-thumb]:shadow-neutral-300"]))
-
-(defn get-slider-bg-classes
-  [service-color]
-  (case service-color
-    "#cc0000" ["[&::-webkit-slider-thumb]:bg-[#cc0000]"
-               "[&::-moz-range-thumb]:bg-[#cc0000]"]
-    "#ff7700" ["[&::-webkit-slider-thumb]:bg-[#ff7700]"
-               "[&::-moz-range-thumb]:bg-[#ff7700]"]
-    "#333333" ["[&::-webkit-slider-thumb]:bg-[#333333]"
-               "[&::-moz-range-thumb]:bg-[#333333]"]
-    "#F2690D" ["[&::-webkit-slider-thumb]:bg-[#F2690D]"
-               "[&::-moz-range-thumb]:bg-[#F2690D]"]
-    "#629aa9" ["[&::-webkit-slider-thumb]:bg-[#629aa9]"
-               "[&::-moz-range-thumb]:bg-[#629aa9]"]
-    ["[&::-webkit-slider-thumb]:bg-neutral-300"
-     "[&::-moz-range-thumb]:bg-neutral-300"]))
-
 (defn button
   [& {:keys [icon on-click disabled? show-on-mobile? extra-classes]}]
   [:button.outline-none.focus:ring-transparent
@@ -91,48 +42,72 @@
    :extra-classes [:text-sm]
    :show-on-mobile? show-on-mobile?])
 
+(defonce slider-classes
+  ["h-2" "cursor-pointer" "appearance-none" "rounded-full"
+   "overflow-hidden"
+   "bg-neutral-300" "dark:bg-neutral-600" "focus:outline-none"
+   "[&::-webkit-slider-runnable-track]:h-2"
+   "[&::-webkit-slider-runnable-track]:bg-[linear-gradient(to_right,#A3A3A3_var(--buffered),#D4D4D4_var(--buffered))]"
+   "dark:[&::-webkit-slider-runnable-track]:bg-[linear-gradient(to_right,#737373_var(--buffered),#525252_var(--buffered))]"
+   "[&::-webkit-slider-thumb]:appearance-none"
+   "[&::-webkit-slider-thumb]:border-0"
+   "[&::-webkit-slider-thumb]:rounded"
+   "[&::-webkit-slider-thumb]:h-2"
+   "[&::-webkit-slider-thumb]:w-2"
+   "[&::-webkit-slider-thumb]:shadow-[-405px_0_0_400px]"
+   "[&::-webkit-slider-thumb]:shadow-[var(--thumb-bg)]"
+   "[&::-webkit-slider-thumb]:bg-[var(--thumb-bg)]"
+   "[&::-moz-range-track]:h-2"
+   "[&::-moz-range-track]:bg-[linear-gradient(to_right,#A3A3A3_var(--buffered),#D4D4D4_var(--buffered))]"
+   "dark:[&::-moz-range-track]:bg-[linear-gradient(to_right,#737373_var(--buffered),#525252_var(--buffered))]"
+   "[&::-moz-range-thumb]:border-0"
+   "[&::-moz-range-thumb]:rounded"
+   "[&::-moz-range-thumb]:h-2"
+   "[&::-moz-range-thumb]:w-2"
+   "[&::-moz-range-thumb]:shadow-[-405px_0_0_400px]"
+   "[&::-moz-range-thumb]:shadow-[var(--thumb-bg)]"
+   "[&::-moz-range-thumb]:bg-[var(--thumb-bg)]"])
+
 (defn time-slider
   [!player !elapsed-time service-color]
-  (let [styles           (concat base-slider-classes
-                                 (get-slider-bg-classes service-color)
-                                 (get-slider-shadow-classes service-color))
-        bg-player-ready? @(rf/subscribe [:bg-player/ready])]
+  (let [bg-player-ready? @(rf/subscribe [:bg-player/ready])
+        !buffered        @(rf/subscribe [:bg-player/buffered])
+        max-value        (if (and bg-player-ready?
+                                  @!player
+                                  (not (js/isNaN (.-duration @!player))))
+                           (.floor js/Math (.-duration @!player))
+                           100)]
     [:input.w-full
-     {:class     styles
+     {:style     {"--buffered" (str @!buffered "%")
+                  "--thumb-bg" service-color}
+      :class     slider-classes
       :type      "range"
       :on-input  #(reset! !elapsed-time (.. % -target -value))
       :on-change #(when (and bg-player-ready? @!player)
                     (set! (.-currentTime @!player) @!elapsed-time))
-      :max       (if (and bg-player-ready?
-                          @!player
-                          (not (js/isNaN (.-duration @!player))))
-                   (.floor js/Math (.-duration @!player))
-                   100)
+      :max       max-value
       :value     @!elapsed-time}]))
 
 (defn volume-slider
   []
   (let [show-slider? (r/atom nil)]
     (fn [player volume-level muted? service-color]
-      (let [styles (concat ["rotate-[270deg]"]
-                           base-slider-classes
-                           (get-slider-bg-classes service-color)
-                           (get-slider-shadow-classes service-color))]
-        [:div.relative.flex.flex-col.justify-center.items-center
-         {:on-mouse-over #(reset! show-slider? true)
-          :on-mouse-out  #(reset! show-slider? false)}
-         [button
-          :icon
-          (if muted? [:i.fa-solid.fa-volume-xmark] [:i.fa-solid.fa-volume-low])
-          :on-click #(rf/dispatch [:bg-player/mute (not muted?) player])]
-         (when @show-slider?
-           [:input.absolute.w-24.ml-2.m-1.bottom-16
-            {:class    (str/join " " styles)
-             :type     "range"
-             :on-input #(rf/dispatch [:player/change-volume
-                                      (.. % -target -value) player])
-             :max      100
-             :value    volume-level}])]))))
+      [:div.relative.flex.flex-col.justify-center.items-center
+       {:on-mouse-over #(reset! show-slider? true)
+        :on-mouse-out  #(reset! show-slider? false)}
+       [button
+        :icon
+        (if muted? [:i.fa-solid.fa-volume-xmark] [:i.fa-solid.fa-volume-low])
+        :on-click #(rf/dispatch [:bg-player/mute (not muted?) player])]
+       (when @show-slider?
+         [:input.absolute.w-24.ml-2.m-1.bottom-16
+          {:style    {"--thumb-bg" service-color}
+           :class    (concat ["rotate-[270deg]"] slider-classes)
+           :type     "range"
+           :on-input #(rf/dispatch [:player/change-volume (.. % -target -value)
+                                    player])
+           :max      100
+           :value    volume-level}])])))
 
 (defn metadata
   [{:keys [thumbnails url name uploader-url uploader-name]}]
@@ -160,8 +135,8 @@
         loading?         @(rf/subscribe [:bg-player/loading])
         loop-playback    @(rf/subscribe [:player/loop])
         shuffle?         @(rf/subscribe [:player/shuffled])
-        bg-player-ready? @(rf/subscribe [:bg-player/ready])
         paused?          @(rf/subscribe [:player/paused])
+        bg-player-ready? @(rf/subscribe [:bg-player/ready])
         !elapsed-time    @(rf/subscribe [:elapsed-time])]
     [:div.flex.flex-col.items-center.ml-auto
      [:div.flex.justify-end.gap-x-4
@@ -268,7 +243,8 @@
   []
   (let [!elapsed-time @(rf/subscribe [:elapsed-time])
         queue-pos     @(rf/subscribe [:queue/position])
-        stream        @(rf/subscribe [:queue/current])]
+        stream        @(rf/subscribe [:queue/current])
+        !buffered     @(rf/subscribe [:bg-player/buffered])]
     (r/create-class
      {:component-will-unmount #(rf/dispatch [:bg-player/ready false])
       :component-did-mount
@@ -283,16 +259,36 @@
                     :content))))
       :reagent-render
       (fn [!player]
-        [:audio
-         {:ref            #(reset! !player %)
-          :loop           (= @(rf/subscribe [:player/loop]) :stream)
-          :muted          @(rf/subscribe [:player/muted])
-          :on-can-play    #(rf/dispatch [:bg-player/ready true])
-          :on-seeked      #(reset! !elapsed-time (.-currentTime @!player))
-          :on-time-update #(reset! !elapsed-time (.-currentTime @!player))
-          :on-play        #(rf/dispatch [:bg-player/set-paused false])
-          :on-pause       #(rf/dispatch [:bg-player/set-paused true])
-          :on-loaded-data #(rf/dispatch [:bg-player/start])}])})))
+        (let [on-progress
+              #(let [len (.. @!player -buffered -length)]
+                 (when (and (.-duration @!player) (> len 0))
+                   (if (= (.end (.-buffered @!player) (- len 1))
+                          (.-duration @!player))
+                     (reset! !buffered 100)
+                     (when (< (.start (.-buffered @!player) (- len 1))
+                              (.-currentTime @!player))
+                       (reset! !buffered (* (/ (.end (.-buffered @!player)
+                                                     (- len 1))
+                                               (.-duration @!player))
+                                            100))))))
+              on-update (fn []
+                          (when @(rf/subscribe [:bg-player/loading])
+                            (rf/dispatch [:bg-player/set-loading false]))
+                          (on-progress)
+                          (reset! !elapsed-time (.-currentTime @!player)))]
+          [:audio
+           {:ref            #(reset! !player %)
+            :preload        "metadata"
+            :on-waiting     #(rf/dispatch [:bg-player/set-loading true])
+            :loop           (= @(rf/subscribe [:player/loop]) :stream)
+            :muted          @(rf/subscribe [:player/muted])
+            :on-can-play    #(rf/dispatch [:bg-player/ready true])
+            :on-seeked      #(reset! !elapsed-time (.-currentTime @!player))
+            :on-progress    on-progress
+            :on-time-update on-update
+            :on-loaded-data #(rf/dispatch [:bg-player/start])
+            :on-play        #(rf/dispatch [:bg-player/set-paused false])
+            :on-pause       #(rf/dispatch [:bg-player/set-paused true])}]))})))
 
 (defn player
   []