Переглянути джерело

feat: replace vidstack with Media Chrome

Miguel Ángel Moreno 4 місяців тому
батько
коміт
c6c5f094c4

+ 31 - 44
package-lock.json

@@ -6,7 +6,9 @@
     "": {
       "dependencies": {
         "@fortawesome/fontawesome-free": "^6.4.2",
-        "@vidstack/react": "^1.11.24",
+        "hls-video-element": "^1.2.11",
+        "hls.js": "^1.5.17",
+        "media-chrome": "^4.2.3",
         "react": "^18.2.0",
         "react-dom": "^18.2.0",
         "timeago.js": "^4.0.2"
@@ -287,37 +289,6 @@
         "undici-types": "~5.26.4"
       }
     },
-    "node_modules/@types/prop-types": {
-      "version": "15.7.12",
-      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
-      "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
-      "peer": true
-    },
-    "node_modules/@types/react": {
-      "version": "18.3.3",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
-      "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
-      "peer": true,
-      "dependencies": {
-        "@types/prop-types": "*",
-        "csstype": "^3.0.2"
-      }
-    },
-    "node_modules/@vidstack/react": {
-      "version": "1.11.24",
-      "resolved": "https://registry.npmjs.org/@vidstack/react/-/react-1.11.24.tgz",
-      "integrity": "sha512-7tgJUJdIKIidzziZ9QqpU73g0BI3BvgYnIim3FrHsxFMRUgZbLKLbSJP1c16NQijS6kljUTWPJErCOm+n27xPg==",
-      "dependencies": {
-        "media-captions": "^1.0.1"
-      },
-      "engines": {
-        "node": ">=18"
-      },
-      "peerDependencies": {
-        "@types/react": "^18.0.0",
-        "react": "^18.0.0"
-      }
-    },
     "node_modules/@webassemblyjs/ast": {
       "version": "1.12.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
@@ -1510,11 +1481,10 @@
       "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
       "dev": true
     },
-    "node_modules/csstype": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
-      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
-      "peer": true
+    "node_modules/custom-media-element": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/custom-media-element/-/custom-media-element-1.4.1.tgz",
+      "integrity": "sha512-kWPRk+6tEebklkd9QWbeCAEOzgL72Uhra4ZSmjN3R9mxg4emqh/0vKyKgxzLtuak76SiaSstzCY6zFrBZ8kyJg=="
     },
     "node_modules/define-data-property": {
       "version": "1.1.4",
@@ -2184,6 +2154,21 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/hls-video-element": {
+      "version": "1.2.11",
+      "resolved": "https://registry.npmjs.org/hls-video-element/-/hls-video-element-1.2.11.tgz",
+      "integrity": "sha512-QaUPnGcJj9uW9US7+XtUMda/i6Wvf9EOTPRCMYzgQKU0h65QgiOpMnfKQDVDZbTanc7333nf2H0ubKyL0umyRg==",
+      "dependencies": {
+        "custom-media-element": "^1.4.1",
+        "hls.js": "^1.5.11",
+        "media-tracks": "^0.3.3"
+      }
+    },
+    "node_modules/hls.js": {
+      "version": "1.5.17",
+      "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.17.tgz",
+      "integrity": "sha512-wA66nnYFvQa1o4DO/BFgLNRKnBTVXpNeldGRBJ2Y0SvFtdwvFKCbqa9zhHoZLoxHhZ+jYsj3aIBkWQQCPNOhMw=="
+    },
     "node_modules/hmac-drbg": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@@ -2591,13 +2576,15 @@
       "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
       "dev": true
     },
-    "node_modules/media-captions": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/media-captions/-/media-captions-1.0.4.tgz",
-      "integrity": "sha512-cyDNmuZvvO4H27rcBq2Eudxo9IZRDCOX/I7VEyqbxsEiD2Ei7UYUhG/Sc5fvMZjmathgz3fEK7iAKqvpY+Ux1w==",
-      "engines": {
-        "node": ">=16"
-      }
+    "node_modules/media-chrome": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.2.3.tgz",
+      "integrity": "sha512-gzwFy2b+RLsEtnPzUzqzf2L5XkaTLQr8POOyLOcoebWSAWg31cPy2vfXNiUnd93sc5IxwJ8OAwkKxnaJNZ8Gjg=="
+    },
+    "node_modules/media-tracks": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/media-tracks/-/media-tracks-0.3.3.tgz",
+      "integrity": "sha512-9P2FuUHnZZ3iji+2RQk7Zkh5AmZTnOG5fODACnjhCVveX1McY3jmCRHofIEI+yTBqplz7LXy48c7fQ3Uigp88w=="
     },
     "node_modules/merge-stream": {
       "version": "2.0.0",

+ 3 - 1
package.json

@@ -6,7 +6,9 @@
   },
   "dependencies": {
     "@fortawesome/fontawesome-free": "^6.4.2",
-    "@vidstack/react": "^1.11.24",
+    "hls-video-element": "^1.2.11",
+    "hls.js": "^1.5.17",
+    "media-chrome": "^4.2.3",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "timeago.js": "^4.0.2"

+ 4 - 4
postcss.config.js

@@ -1,8 +1,8 @@
 module.exports = {
   plugins: {
-    'postcss-import': {},
+    "postcss-import": {},
     tailwindcss: {},
     autoprefixer: {},
-    cssnano: process.env.NODE_ENV === 'production' ? {} : false
-  }
-}
+    cssnano: process.env.NODE_ENV === "production" ? {} : false,
+  },
+};

+ 12 - 4
resources/src/styles/index.scss

@@ -1,14 +1,22 @@
 @use "@fontsource/nunito-sans/scss/mixins" as NunitoSans;
 $fontsourceDir: "~@fontsource";
-@include NunitoSans.faces($weights: (200, 300, 400, 500, 600, 700, 800, 900));
+@include NunitoSans.faces(
+  $weights: (
+    200,
+    300,
+    400,
+    500,
+    600,
+    700,
+    800,
+    900,
+  )
+);
 $fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
 @import "@fortawesome/fontawesome-free/scss/brands";
 @import "@fortawesome/fontawesome-free/scss/regular";
 @import "@fortawesome/fontawesome-free/scss/solid";
 @import "@fortawesome/fontawesome-free/scss/fontawesome";
-@import '@vidstack/react/player/styles/default/theme.css';
-@import '@vidstack/react/player/styles/default/layouts/video.css';
-@import '@vidstack/react/player/styles/default/layouts/audio.css';
 @import "tailwindcss/base";
 @import "tailwindcss/components";
 @import "tailwindcss/utilities";

+ 5 - 2
shadow-cljs.edn

@@ -3,7 +3,10 @@
                   :proxy-url "http://localhost:3000"}}
  :builds
  {:tubo
-  {:target :browser
+  {:target     :browser
    :output-dir "resources/public/js"
    :asset-path "/js"
-   :modules {:main {:init-fn tubo.core/init}}}}}
+   :js-options {:entry-keys        ["module" "browser" "main"]
+                :export-conditions ["import" "module" "browser" "require"
+                                    "default"]}
+   :modules    {:main {:init-fn tubo.core/init}}}}}

+ 4 - 20
src/frontend/tubo/bg_player/events.cljs

@@ -19,36 +19,20 @@
  :bg-player/pause
  [(rf/inject-cofx ::inject/sub [:bg-player])]
  (fn [{:keys [bg-player]} [_ paused?]]
-   {:player/pause {:paused? paused?
+   {:player/pause {:paused? (not paused?)
                    :player  bg-player}}))
 
-(rf/reg-event-fx
- :bg-player/play
- [(rf/inject-cofx ::inject/sub [:elapsed-time])
-  (rf/inject-cofx ::inject/sub [:main-player])]
- (fn [{:keys [main-player db elapsed-time]}]
-   {:fx [[:dispatch [:bg-player/set-paused false]]
-         [:dispatch [:bg-player/seek @elapsed-time]]
-         (when (and (:main-player/ready db) main-player @main-player)
-           [:dispatch [:main-player/pause true]])]}))
-
-(rf/reg-event-fx
- :bg-player/stop
- (fn [_]
-   {:fx [[:dispatch [:bg-player/pause true]]
-         [:dispatch [:bg-player/seek 0]]]}))
-
 (rf/reg-event-fx
  :bg-player/start
  [(rf/inject-cofx ::inject/sub [:bg-player])
   (rf/inject-cofx ::inject/sub [:elapsed-time])]
- (fn [{:keys [db bg-player]} _]
+ (fn [{:keys [db bg-player elapsed-time]} _]
    {:fx [[:dispatch [:bg-player/set-paused true]]
+         [:dispatch [:bg-player/seek @elapsed-time]]
          [:dispatch [:bg-player/pause false]]
          [:dispatch
           [:player/change-volume (:player/volume db)
-           bg-player]]
-        ]}))
+           bg-player]]]}))
 
 (rf/reg-event-fx
  :bg-player/mute

+ 24 - 48
src/frontend/tubo/bg_player/views.cljs

@@ -2,14 +2,12 @@
   (:require
    [clojure.string :as str]
    [re-frame.core :as rf]
+   [reagent.dom :as rdom]
    [reagent.core :as r]
    [reitit.frontend.easy :as rfe]
    [tubo.bookmarks.modals :as modals]
    [tubo.layout.views :as layout]
-   [tubo.utils :as utils]
-   ["@vidstack/react" :refer (MediaPlayer MediaProvider)]
-   ["@vidstack/react/player/layouts/default" :refer
-    (defaultLayoutIcons DefaultAudioLayout)]))
+   [tubo.utils :as utils]))
 
 (defonce base-slider-classes
   ["h-2" "cursor-pointer" "appearance-none" "bg-neutral-300"
@@ -180,8 +178,7 @@
            [:i.fa-solid.fa-play]
            [:i.fa-solid.fa-pause])
          [layout/loading-icon color "lg:text-2xl"])
-       :on-click
-       #(rf/dispatch [:bg-player/pause (not (.-paused @!player))])
+       :on-click #(rf/dispatch [:bg-player/pause (not (.-paused @!player))])
        :show-on-mobile? true
        :extra-classes ["lg:text-2xl"]]
       [button
@@ -258,52 +255,31 @@
           :menu-styles {:bottom "30px" :top nil :right "10px"}
           :extra-classes [:pt-1 :!pl-4 :px-3]]]))))
 
-(defn get-audio-player-sources
-  [available-streams]
-  (if available-streams
-    (->> available-streams
-         (filter #(not= (:format %) "OPUS"))
-         (sort-by :bitrate)
-         (map (fn [{:keys [content]}] {:src content :type "audio/mpeg"})))
-    []))
-
 (defn audio-player
-  [_ _]
-  (let [!elapsed-time     @(rf/subscribe [:elapsed-time])
-        !bg-player-first? (r/atom nil)]
+  [_]
+  (let [!elapsed-time @(rf/subscribe [:elapsed-time])
+        queue-pos     @(rf/subscribe [:queue/position])
+        stream        @(rf/subscribe [:queue/current])]
     (r/create-class
      {:component-will-unmount #(rf/dispatch [:bg-player/ready false])
+      :component-did-mount
+      (fn [this]
+        (set! (.-onended (rdom/dom-node this))
+              #(rf/dispatch [:queue/change-pos (inc queue-pos)]))
+        (when stream
+          (set! (.-src (rdom/dom-node this))
+                (:content (nth (:audio-streams stream) 0)))))
       :reagent-render
-      (fn [{:keys [name audio-streams]} !player]
-        [:> MediaPlayer
-         {:title          name
-          :class          "invisible fixed"
-          :controls       []
-          :src            (get-audio-player-sources audio-streams)
-          :viewType       "audio"
-          :ref            #(reset! !player %)
+      (fn [!player]
+        [:audio
+         {:ref            #(reset! !player %)
           :loop           (= @(rf/subscribe [:player/loop]) :stream)
-          :onCanPlay      #(rf/dispatch [:bg-player/ready true])
-          :onSeeked       #(reset! !elapsed-time (.-currentTime @!player))
-          :onTimeUpdate   #(reset! !elapsed-time (.-currentTime @!player))
-          :onEnded        (fn []
-                            (rf/dispatch [:queue/change-pos
-                                          (inc @(rf/subscribe
-                                                 [:queue/position]))])
-                            (reset! !elapsed-time 0))
-          :onPlay         #(rf/dispatch [:bg-player/play])
-          :onReplay       (fn []
-                            (rf/dispatch [:bg-player/set-paused false])
-                            (reset! !elapsed-time 0))
-          :onPause        #(rf/dispatch [:bg-player/set-paused true])
-          :onLoadedData   (fn []
-                            (rf/dispatch [:bg-player/start])
-                            (when-not @!bg-player-first?
-                              (reset! !bg-player-first? true)))
-          :onSourceChange #(when @!bg-player-first?
-                             (reset! !elapsed-time 0))}
-         [:> MediaProvider]
-         [:> DefaultAudioLayout {:icons defaultLayoutIcons}]])})))
+          :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])}])})))
 
 (defn player
   []
@@ -335,7 +311,7 @@
          :background-position "center"
          :background-repeat   "no-repeat"}}
        [:div.flex.items-center
-        [audio-player stream !player]
+        [audio-player !player]
         [metadata stream]
         [main-controls !player color]
         [extra-controls !player stream color]]])))

+ 1 - 1
src/frontend/tubo/main_player/events.cljs

@@ -14,7 +14,7 @@
  [(rf/inject-cofx ::inject/sub [:main-player])]
  (fn [{:keys [db main-player]} [_ paused?]]
    (when (:main-player/ready db)
-     {:player/pause {:paused? paused?
+     {:player/pause {:paused? (not paused?)
                      :player  main-player}})))
 
 (rf/reg-event-fx

+ 7 - 3
src/frontend/tubo/player/events.cljs

@@ -17,8 +17,10 @@
 
 (rf/reg-fx
  :player/src
- (fn [{:keys [player src]}]
-   (set! (.-source @player) (clj->js src))))
+ (fn [{:keys [player src current-pos]}]
+   (set! (.-src @player) (clj->js src))
+   (set! (.-onended @player)
+         #(rf/dispatch [:queue/change-pos (inc current-pos)]))))
 
 (rf/reg-fx
  :player/loop
@@ -35,7 +37,9 @@
  :player/pause
  (fn [{:keys [paused? player]}]
    (when (and player @player)
-     (set! (.-paused @player) paused?))))
+     (if paused?
+       (.play @player)
+       (.pause @player)))))
 
 (rf/reg-fx
  :media-session-metadata

+ 103 - 58
src/frontend/tubo/player/views.cljs

@@ -2,27 +2,17 @@
   (:require
    [re-frame.core :as rf]
    [reagent.core :as r]
-   ["@vidstack/react" :refer (MediaPlayer MediaProvider Poster)]
-   ["@vidstack/react/player/layouts/default" :refer
-    (defaultLayoutIcons DefaultVideoLayout)]))
-
-(defn get-video-player-sources
-  [available-streams service-id]
-  (if available-streams
-    (if (= service-id 3)
-      (map (fn [{:keys [content]}] {:src content :type "video/mp4"})
-           (reverse available-streams))
-      (->> available-streams
-           (filter #(and (not= (:format %) "WEBMA_OPUS")
-                         (not= (:format %) "OPUS")
-                         (not= (:format %) "M4A")))
-           (sort-by :bitrate)
-           (#(if (empty? (filter (fn [x] (= (:format x) "MP3")) %))
-               (reverse %)
-               %))
-           (map (fn [{:keys [content]}] {:src content :type "video/mp4"}))
-           first))
-    []))
+   ["media-chrome/dist/react" :refer
+    (MediaController
+     MediaControlBar
+     MediaTimeRange
+     MediaTimeDisplay
+     MediaVolumeRange
+     MediaFullscreenButton
+     MediaPipButton
+     MediaPlayButton
+     MediaPlaybackRateButton
+     MediaMuteButton)]))
 
 (defn video-player
   [_stream _!player]
@@ -31,41 +21,96 @@
     (r/create-class
      {:component-will-unmount #(rf/dispatch [:main-player/ready false])
       :reagent-render
-      (fn [{:keys [name video-streams audio-streams thumbnail-url service-id]}
+      (fn [{:keys [video-streams audio-streams thumbnail-url]}
            !player]
-        (let [show-main-player? @(rf/subscribe [:main-player/show])]
-          [:> MediaPlayer
-           {:title          name
-            :src            (get-video-player-sources (into video-streams
-                                                            audio-streams)
-                                                      service-id)
-            :poster         thumbnail-url
-            :class          "w-full xl:w-3/5 overflow-hidden"
-            :playsInline    true
-            :ref            #(reset! !player %)
-            :loop           (when show-main-player?
-                              (= @(rf/subscribe [:player/loop]) :stream))
-            :onSeeked       (when show-main-player?
-                              #(reset! !elapsed-time (.-currentTime @!player)))
-            :onTimeUpdate   (when show-main-player?
-                              #(reset! !elapsed-time (.-currentTime @!player)))
-            :onEnded        #(when show-main-player?
-                               (rf/dispatch [:queue/change-pos
-                                             (inc @(rf/subscribe
-                                                    [:queue/position]))])
-                               (reset! !elapsed-time 0))
-            :onLoadedData   (fn []
-                              (when show-main-player?
-                                (rf/dispatch [:main-player/start]))
-                              (when (and @!main-player-first? show-main-player?)
-                                (reset! !main-player-first? false)))
-            :onPlay         #(rf/dispatch [:main-player/play])
-            :onCanPlay      #(rf/dispatch [:main-player/ready true])
-            :onSourceChange #(when-not @!main-player-first?
-                               (reset! !elapsed-time 0))}
-           [:> MediaProvider
-            [:> Poster
-             {:src   thumbnail-url
-              :alt   name
-              :class :vds-poster}]]
-           [:> DefaultVideoLayout {:icons defaultLayoutIcons}]]))})))
+        (let [show-main-player? @(rf/subscribe [:main-player/show])
+              service-color     @(rf/subscribe [:service-color])]
+          [:div
+           {:class "w-full h-80 md:h-[450px] lg:h-[600px]"}
+           [:> MediaController
+            {:style {"--media-secondary-color" "transparent"
+                     "--media-primary-color"   "white"
+                     "aspectRatio"             "16/9"
+                     "height"                  "100%"
+                     "width"                   "100%"}}
+            [:video
+             {:style          {"max-height" "100%"
+                               "min-height" "100%"
+                               "min-width"  "100%"
+                               "max-width"  "100%"}
+              :ref            #(reset! !player %)
+              :poster         thumbnail-url
+              :loop           (when show-main-player?
+                                (= @(rf/subscribe [:player/loop]) :stream))
+              :on-can-play    #(rf/dispatch [:main-player/ready true])
+              :on-ended       #(when show-main-player?
+                                 (rf/dispatch [:queue/change-pos
+                                               (inc @(rf/subscribe
+                                                      [:queue/position]))])
+                                 (reset! !elapsed-time 0))
+              :on-play        #(rf/dispatch [:main-player/play])
+              :on-loaded-data (fn []
+                                (when show-main-player?
+                                  (rf/dispatch [:main-player/start]))
+                                (when (and @!main-player-first?
+                                           show-main-player?)
+                                  (reset! !main-player-first? false)))
+              :on-time-update (when show-main-player?
+                                #(reset! !elapsed-time (.-currentTime
+                                                        @!player)))
+              :on-seeked      (when show-main-player?
+                                #(reset! !elapsed-time (.-currentTime
+                                                        @!player)))
+              :slot           "media"
+              :src            (:content (nth (into video-streams audio-streams)
+                                             0))
+              :preload        "auto"
+              :muted          @(rf/subscribe [:player/muted])
+              :crossOrigin    ""}]
+            [:div.ytp-gradient-bottom.absolute.w-full.bottom-0.pointer-events-none.bg-bottom.bg-repeat-x
+             {:style
+              {"paddingTop" "37px"
+               "height" "170px"
+               "backgroundImage"
+               "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAACqCAYAAABsziWkAAAAAXNSR0IArs4c6QAAAQVJREFUOE9lyNdHBQAAhfHb3nvvuu2997jNe29TJJEkkkgSSSSJJJJEEkkiifRH5jsP56Xz8PM5gcC/xfDEmjhKxEOCSaREEiSbFEqkQppJpzJMJiWyINvkUCIX8kw+JQqg0BRRxaaEEqVQZsopUQGVpooS1VBjglStqaNEPTSYRko0QbNpoUQrtJl2qsN0UqILuk0PJXqhz/RTYgAGzRA1bEYoMQpjZpwSExAyk5SYgmkzQ82aOUqEIWKilJiHBbNIiSVYhhVYhTVYhw3YhC3Yhh3YhT3YhwM4hCM4hhM4hTM4hwu4hCu4hhu4hTu4hwd4hCd4hhd4hTd4hw/4hC/4hh/4/QM2/id28uIEJAAAAABJRU5ErkJggg==')"}}]
+            [:> MediaTimeRange
+             {:class "w-full h-[5px]"
+              :style
+              {"--media-control-hover-background" "transparent"
+               "--media-range-track-transition" "height 0.1s linear"
+               "--media-range-track-background" "rgba(255,255,255,.2)"
+               "--media-range-track-pointer-background" "rgba(255,255,255,.5)"
+               "--media-time-range-buffered-color" "rgba(255,255,255,.4)"
+               "--media-range-bar-color" service-color
+               "--media-range-thumb-border-radius" "13px"
+               "--media-range-thumb-background" service-color
+               "--media-range-thumb-transition" "transform 0.1s linear"
+               "--media-range-thumb-transform" "scale(0) translate(0%, 0%)"}}]
+            [:> MediaControlBar
+             {:class "relative pl-[10px] pr-[5px]"
+              :style
+              {"--media-control-hover-background"  "transparent"
+               "--media-range-track-height"        "3px"
+               "--media-range-thumb-height"        "13px"
+               "--media-range-thumb-width"         "13px"
+               "--media-range-thumb-border-radius" "13px"}}
+             [:> MediaPlayButton
+              {:class "py-[6px] px-[10px]"
+               :style
+               {"--media-button-icon-width" "30px"}}]
+             [:> MediaMuteButton
+              {:class "peer/mute"}]
+             [:> MediaVolumeRange
+              {:class
+               ["w-0" "overflow-hidden" "transition-[width]"
+                "transition-200" "ease-in" "peer-hover/mute:w-[70px]"
+                "peer-focus/mute:w-[70px]" "hover:w-[70px]" "focus:w-[70px]"]
+               :style
+               {"--media-range-track-background" "rgba(255,255,255,.2)"
+                "--media-range-bar-color"        service-color
+                "--media-range-thumb-background" service-color}}]
+             [:> MediaTimeDisplay {:showDuration true}]
+             [:span.control-spacer.grow]
+             [:> MediaPlaybackRateButton]
+             [:> MediaPipButton]
+             [:> MediaFullscreenButton]]]]))})))

+ 13 - 5
src/frontend/tubo/queue/events.cljs

@@ -1,6 +1,7 @@
 (ns tubo.queue.events
   (:require
-   [re-frame.core :as rf]))
+   [re-frame.core :as rf]
+   [vimsical.re-frame.cofx.inject :as inject]))
 
 (rf/reg-event-fx
  :queue/show
@@ -104,8 +105,15 @@
 
 (rf/reg-event-fx
  :queue/change-stream
- [(rf/inject-cofx :store)]
- (fn [{:keys [db store]} [_ stream idx]]
+ [(rf/inject-cofx :store)
+  (rf/inject-cofx ::inject/sub [:bg-player])]
+ (fn [{:keys [db store bg-player]} [_ stream idx]]
    (let [update-entry (fn [x] (update-in x [:queue idx] #(merge % stream)))]
-     {:db    (assoc (update-entry db) :queue/position idx)
-      :store (assoc (update-entry store) :queue/position idx)})))
+     {:db         (assoc (update-entry db) :queue/position idx)
+      :store      (assoc (update-entry store) :queue/position idx)
+      :player/src {:player      bg-player
+                   :src         (-> stream
+                                    :audio-streams
+                                    first
+                                    :content)
+                   :current-pos idx}})))

+ 3 - 7
tailwind.config.js

@@ -1,4 +1,3 @@
-/** @type {import('tailwindcss').Config} */
 module.exports = {
   content: ["./src/**/*.{html,js,cljs}"],
   darkMode: "class",
@@ -8,12 +7,9 @@ module.exports = {
         "nunito-sans": ["Nunito Sans", "sans-serif"],
       },
       screens: {
-        "xs": "480px",
+        xs: "480px",
       },
     },
   },
-  plugins: [
-    require("@tailwindcss/forms"),
-    require("@vidstack/react/tailwind.cjs")
-  ],
-}
+  plugins: [require("@tailwindcss/forms")],
+};